package com.bestcem.xm.qdes.service.impl;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.bestcem.xm.common.core.exception.BusinessException;
import com.bestcem.xm.common.core.oss.enums.OssPathEnum;
import com.bestcem.xm.common.core.sql.ConditionDataConverter;
import com.bestcem.xm.common.core.uitls.DateUtil;
import com.bestcem.xm.component.mybatis.service.impl.XmServiceImpl;
import com.bestcem.xm.componet.filestorage.service.StorageService;
import com.bestcem.xm.qdes.controller.param.*;
import com.bestcem.xm.qdes.controller.vo.*;
import com.bestcem.xm.qdes.convert.QdesProjectConvertMapper;
import com.bestcem.xm.qdes.convert.QstructConvertMapper;
import com.bestcem.xm.qdes.entity.*;
import com.bestcem.xm.qdes.enums.FileInfoFTypeEnum;
import com.bestcem.xm.qdes.enums.ProjectPermissionEnum;
import com.bestcem.xm.qdes.grpc.client.SurveyGrpcClient;
import com.bestcem.xm.qdes.grpc.client.UserGrpcClient;
import com.bestcem.xm.qdes.grpc.v1.services.ProjectEntryStatus;
import com.bestcem.xm.qdes.mapper.*;
import com.bestcem.xm.qdes.mq.dto.CreateProjectDto;
import com.bestcem.xm.qdes.mq.dto.StatusDto;
import com.bestcem.xm.qdes.mq.send.biz.QdesMessageSendService;
import com.bestcem.xm.qdes.service.QdesProjectService;
import com.bestcem.xm.qdes.service.QdesQstructService;
import com.bestcem.xm.qdes.service.QdesQuestionService;
import com.bestcem.xm.qdes.service.ServiceSequenceService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static com.bestcem.xm.qdes.enums.ProjectBadgeStatusEnum.PROJECTENTRY_BADGE_STATUS_READ;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectEntryCategory.PROJECTENTRY_CATEGORY_MANAGER;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectEntryPermType.PROJECTENTRY_PERM_TYPE_CUSTOM;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectPtype.PROJECT_PTYPE_QUESITON;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectPtype.PROJECT_PTYPE_TEMPLET;
import static com.bestcem.xm.qdes.grpc.v1.services.QStructStatus.QSTRUCT_STATUS_DRAFT;
import static com.bestcem.xm.qdes.grpc.v1.services.QStructStatus.QSTRUCT_STATUS_DRAFT_VALUE;
import static com.bestcem.xm.qdes.grpc.v1.services.TagRefRefType.TAGREF_REF_TYPE_LIBRARY;
import static com.bestcem.xm.qdes.util.QstructConstant.PROJ_EDITABLE_STATUS_LIST;

@Slf4j
@Service
//@RequiredArgsConstructor
@RequiredArgsConstructor(onConstructor_ = {@Lazy, @Autowired})
public class QdesQstructServiceImpl extends XmServiceImpl<QdesQstructMapper, QdesQstruct> implements QdesQstructService {
    private static final String PAGE_FILE_PATH = "file/struct_default_data_p_items.json";

    private @NonNull UploadFileMapMapper uploadFileMapMapper;
    private @NonNull UploadFileInfoMapper uploadFileInfoMapper;
    private @NonNull QdesQuestionMapper qdesQuestionMapper;

    private @NonNull QstructConvertMapper qstructConvertMapper;
    private @NonNull QdesProjectConvertMapper qdesProjectConvertMapper;

    private @NonNull QdesProjectMapper qdesProjectMapper;
    private @NonNull QdesLibProjectMapper qdesLibprojectMapper;
    private @NonNull QdesQstructMapper qdesQstructMapper;
    private @NonNull QdesQuestionLibMapper qdesQuestionlibMapper;
    private @NonNull QdesTagRefMapper qdesTagrefMapper;
    private @NonNull QdesQuotaMapper qdesQuotaMapper;
    private @NonNull QdesProjectEntryMapper qdesProjectentryMapper;

    private @NonNull ServiceSequenceService serviceSequenceService;

    private @NonNull QdesProjectService qdesProjectService;
    // 这里主要是复用getQstructQuestions函数,无数据库操作,不存在嵌套事务,放心大胆的用getQstructQuestions
    private @NonNull QdesQuestionService questionService;
    //private @NonNull OssService ossService;
    private @NonNull StorageService storageService;

    private @NonNull UserGrpcClient userGrpcClient;
    private @NonNull SurveyGrpcClient surveyGrpcClient;
    private @NonNull QdesMessageSendService qdesMessageSendService;

    private static final String ROOT_FILE_PATH = System.getProperty("user.dir");

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> addTemplates(String id, QstructsTemplatesParam qstructsParam) {
        //1:通过qstructId获取projectId,templates
        QdesQstruct qdesQstructInfo = baseMapper.selectById(id);
        String projectId = qdesQstructInfo.getProjectId();
        String templates = qdesQstructInfo.getTemplates();
        List<Map<String, Object>> list = JSON.parseObject(templates, new TypeReference<List<Map<String, Object>>>() {
        });
        //2.插入filemap,用来后续指向编辑模板保存的fileinfo
        UploadFileMap uploadFileMap = new UploadFileMap();
        //uploadFileMap.setProjectId(projectId);
        uploadFileMap.setSysId("qdes");
        uploadFileMapMapper.insert(uploadFileMap);

        Map<String, Object> map = new HashMap<>();
        map.put("id", uploadFileMap.getId());
        map.put("name", qstructsParam.getName());
        if (ObjectUtil.isEmpty(qstructsParam.getQList())) {
            map.put("qList", new ArrayList<>());
        } else {
            map.put("qList", qstructsParam.getQList());
        }
        map.put("filePath", "");
        map.put("qtypeName", qstructsParam.getQtypeName());
        map.put("updated_dt", cn.hutool.core.date.DateUtil.now());
        map.put("custom_qtype", qstructsParam.getCustomGtype());
        list.add(map);
        templates = JSONObject.toJSONString(list);

        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper.eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setTemplates(templates);
        qdesQstruct.setStatus(QSTRUCT_STATUS_DRAFT_VALUE);
        //3.更新templates
        baseMapper.update(qdesQstruct, updateWrapper);
        Map<String, Object> ret = new HashMap<>();
        ret.put("id", uploadFileMap.getId());
        return ret;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void editTemplates(String id, String tid, QstructsTemplatesParam qstructsParam) throws Exception {
        UploadFileInfo uploadFileInfo = new UploadFileInfo();
        String fileInfoId;
        String url;
        //编辑的模板内容
        String template = qstructsParam.getTemplate();

        String fileId = DigestUtils.md5Hex(template);
        //创建查询条件对象
        LambdaQueryWrapper<UploadFileInfo> queryWrapper = Wrappers.lambdaQuery();
        //根据fileId判断文件是否上传过MD5
        queryWrapper.eq(UploadFileInfo::getFileId, fileId)
                .eq(UploadFileInfo::getDeleted, 0)
                .last("limit 1");
        UploadFileInfo isExistUploadFileInfo = uploadFileInfoMapper.selectOne(queryWrapper);
        String filename = fileId.substring(5);
        //1.插upload_fileinfo信息
        if (ObjectUtil.isEmpty(isExistUploadFileInfo)) {
            FileOutputStream out = null;
            try {
                //上传文件
                String path = ROOT_FILE_PATH + File.separator + StrUtil.replace(OssPathEnum.TEMP_QDES.getPath(), "/", File.separator);
                File file1 = new File(path);
                if (!file1.exists()) {
                    file1.mkdirs();
                }
                File fileInfo = new File(path, filename);
                out = new FileOutputStream(fileInfo);
                byte[] templateBytes = template.getBytes();
                out.write(templateBytes);
                //上传文件
                //url = ossService.uploadSimpleFile(fileInfo, filename, OssPathEnum.QDES, 0, null, "");
                url = storageService.upload(fileInfo, filename, OssPathEnum.QDES.getPath(), new Dict()).getUrl();
                uploadFileInfo.setFileId(fileId);
                uploadFileInfo.setFileSize((int) fileInfo.length());
                uploadFileInfo.setFtype(FileInfoFTypeEnum.FILEINFO_FTYPE_FILE.getValue());
                uploadFileInfo.setPolicy(1);
                uploadFileInfo.setUrl(url);
                uploadFileInfoMapper.insert(uploadFileInfo);
                fileInfoId = uploadFileInfo.getId();
                //删除临时文件
                fileInfo.delete();
            } catch (Exception e) {
                throw e;
            } finally {
                if (out != null) {
                    out.close();
                }
            }

        } else {
            fileInfoId = isExistUploadFileInfo.getId();
            url = isExistUploadFileInfo.getUrl();
        }
        //更新filimap里的fileinfoID
        updateFileMap(tid, fileInfoId, filename);

        //更新qstruct里Templates
        String templates = baseMapper.selectById(id).getTemplates();
        List<Map<String, Object>> list = JSON.parseObject(templates, new TypeReference<List<Map<String, Object>>>() {
        });
        List<Map<String, Object>> tempList = new ArrayList<>();
        for (Map<String, Object> map : list) {
            if (map.get("id").equals(tid)) {
                map.put("filePath", url);
                if (ObjectUtil.isNotEmpty(qstructsParam.getQList())) {
                    map.put("qList", qstructsParam.getQList());
                }
                //目前页面模板编辑修改文件名称没卵用，前端没有传修改后的name
                /*if (ObjectUtil.isNotEmpty(qstructsParam.getName())) {
                    map.put("name", qstructsParam.getName());
                }*/
                map.put("updated_dt", cn.hutool.core.date.DateUtil.now());
            }
            tempList.add(map);
        }
        templates = JSONObject.toJSONString(tempList);
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper.eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setTemplates(templates);
        qdesQstruct.setStatus(QSTRUCT_STATUS_DRAFT_VALUE);
        //3.更新templates
        baseMapper.update(qdesQstruct, updateWrapper);

    }

    public void updateFileMap(String tid, String fileInfoId, String filename) {
        //创建修改条件对象
        LambdaUpdateWrapper<UploadFileMap> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(UploadFileMap::getId, tid);
        UploadFileMap uploadFileMap = new UploadFileMap();
        uploadFileMap.setFileInfoId(fileInfoId);
        uploadFileMap.setOriginFileInfoId(fileInfoId);
        uploadFileMap.setFileName(filename);
        //更新uploadFileMap
        uploadFileMapMapper.update(uploadFileMap, updateWrapper);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DeletedCountVo deleteTemplates(String id, String tid) {
        DeletedCountVo deletedCountVo = new DeletedCountVo();
        String templates = baseMapper.selectById(id).getTemplates();
        List<Object> list = JSONArray.parseArray(templates, Object.class);
        List<Object> tempList = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            Map<String, Object> map;
            map = (Map<String, Object>) list.get(i);
            if (!map.get("id").equals(tid)) {
                tempList.add(map);
            }
        }
        templates = JSONObject.toJSONString(tempList);
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setTemplates(templates);
        //更新templates
        int count = baseMapper.update(qdesQstruct, updateWrapper);
        deletedCountVo.setDeletedCount(count);

        //删除upload_filemap,先查询对应的fileinfoId，后续判断是否删除fileinfo
        UploadFileMap uploadFileMap = uploadFileMapMapper.selectById(tid);
        //创建修改条件对象
        LambdaQueryWrapper<UploadFileMap> queryWrapper = new LambdaQueryWrapper<>();
        //构造查询条件
        queryWrapper
                .eq(UploadFileMap::getFileInfoId, uploadFileMap.getFileInfoId());
        long countFileMap = uploadFileMapMapper.selectCount(queryWrapper);

        uploadFileMapMapper.deleteById(tid);
        if (countFileMap == 1) {
            //需要删除upload_fileinfo
            uploadFileInfoMapper.deleteById(uploadFileMap.getFileInfoId());
        }

        return deletedCountVo;
    }

    @Override
    public List<QstructsTemplatesVo> getTemplates(String id, String search) {
        List<QstructsTemplatesVo> qstructsTemplatesVoList;
        String templates = baseMapper.selectById(id).getTemplates();
        List<QstructsTemplatesVo> temp = JSONArray.parseArray(templates, QstructsTemplatesVo.class);
        if (StringUtils.isNotEmpty(search)) {
            qstructsTemplatesVoList = temp.stream().filter(it -> it.getName().contains(search)).collect(Collectors.toList());
        } else {
            qstructsTemplatesVoList = temp;
        }
        /*List<Object> list = JSONArray.parseArray(templates, Object.class);
        for (int i = 0; i < list.size(); i++) {
            QstructsTemplatesVo qstructsTemplatesVo = new QstructsTemplatesVo();
            Map<String, Object> map = (Map<String, Object>) list.get(i);
            if (StringUtils.isNotEmpty(search)) {
                if (map.get("name").toString().contains(search)) {
                    qstructsTemplatesVo.setId((String) map.get("id"));
                    qstructsTemplatesVo.setName((String) map.get("name"));
                    qstructsTemplatesVo.setQList(Arrays.asList(map.get("qList").toString()));
                    qstructsTemplatesVo.setFilePath((String) map.get("filePath"));
                    qstructsTemplatesVo.setQtypeName((String) map.get("qtypeName"));
                    qstructsTemplatesVo.setUpdatedDt((String) map.get("updated_dt"));
                    qstructsTemplatesVo.setCustomGtype((String) map.get("custom_qtype"));
                    qstructsTemplatesVoList.add(qstructsTemplatesVo);
                }
            } else {
                qstructsTemplatesVo.setId((String) map.get("id"));
                qstructsTemplatesVo.setName((String) map.get("name"));
                qstructsTemplatesVo.setQList(Arrays.asList(map.get("qList").toString()));
                qstructsTemplatesVo.setFilePath((String) map.get("filePath"));
                qstructsTemplatesVo.setQtypeName((String) map.get("qtypeName"));
                qstructsTemplatesVo.setUpdatedDt((String) map.get("updated_dt"));
                qstructsTemplatesVo.setCustomGtype((String) map.get("custom_qtype"));
                qstructsTemplatesVoList.add(qstructsTemplatesVo);
            }
        }*/
        return qstructsTemplatesVoList;
    }

    @Override
    public QstructsTemplatesVo getEditTemplates(String id, String tid) {
        QstructsTemplatesVo qstructsTemplatesVo = null;
        String templates = baseMapper.selectById(id).getTemplates();
        List<QstructsTemplatesVo> temp = JSONArray.parseArray(templates, QstructsTemplatesVo.class);
        for (QstructsTemplatesVo templatesVo : temp) {
            if (templatesVo.getId().equals(tid)) {
                qstructsTemplatesVo = templatesVo;
            }
        }
        return qstructsTemplatesVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map addCss(String id, QstructsCssParam cssParam) throws Exception {
        UploadFileInfo uploadFileInfo = new UploadFileInfo();
        String fileInfoId;
        String url;
        //通过qstructId获取projectId
        QdesQstruct qdesQstructInfo = baseMapper.selectById(id);
        String projectId = qdesQstructInfo.getProjectId();
        //编辑的css模板内容
        String css = cssParam.getCss();
        //1.插入fileinfo信息
        String fileId = DigestUtils.md5Hex(css);
        //创建查询条件对象
        LambdaQueryWrapper<UploadFileInfo> queryWrapper = Wrappers.lambdaQuery();
        //根据fileId判断文件是否上传过MD5
        queryWrapper
                .eq(UploadFileInfo::getFileId, fileId)
                .eq(UploadFileInfo::getDeleted, 0).last("limit 1");
        UploadFileInfo isExistUploadFileInfo = uploadFileInfoMapper.selectOne(queryWrapper);
        String filename = fileId.substring(5);
        //1.插upload_fileinfo信息
        if (ObjectUtil.isEmpty(isExistUploadFileInfo)) {
            FileOutputStream out = null;
            try {
                //上传文件
                String path = ROOT_FILE_PATH + File.separator + StrUtil.replace(OssPathEnum.TEMP_QDES.getPath(), "/", File.separator);
                File file1 = new File(path);
                if (!file1.exists()) {
                    file1.mkdirs();
                }
                File fileInfo = new File(path, filename);
                out = new FileOutputStream(fileInfo);
                byte[] cssBytes = css.getBytes();
                out.write(cssBytes);
                //上传文件
                //url = ossService.uploadSimpleFile(fileInfo, filename, OssPathEnum.QDES, 0, null, "");
                url = storageService.upload(fileInfo, filename, OssPathEnum.QDES.getPath(), new Dict()).getUrl();
                uploadFileInfo.setFileId(fileId);
                uploadFileInfo.setFileSize((int) fileInfo.length());
                uploadFileInfo.setFtype(FileInfoFTypeEnum.FILEINFO_FTYPE_FILE.getValue());
                uploadFileInfo.setPolicy(1);
                uploadFileInfo.setUrl(url);
                uploadFileInfoMapper.insert(uploadFileInfo);
                fileInfoId = uploadFileInfo.getId();
                //删除临时文件
                fileInfo.delete();
            } catch (Exception e) {
                throw e;
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        } else {
            fileInfoId = isExistUploadFileInfo.getId();
            url = isExistUploadFileInfo.getUrl();
        }

        //2.插入filemap,用来后续指向编辑模板保存的fileinfo
        UploadFileMap uploadFileMap = new UploadFileMap();
        uploadFileMap.setFileName(filename);
        uploadFileMap.setFileInfoId(fileInfoId);
        uploadFileMap.setOriginFileInfoId(fileInfoId);
        uploadFileMap.setSysId("qdes");
        uploadFileMapMapper.insert(uploadFileMap);

        //3.更新qstruct里gcss
        Map map = new HashMap();
        map.put("id", uploadFileMap.getId());
        map.put("filePath", url);
        String gcss = JSONObject.toJSONString(map);
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setGcss(gcss);
        baseMapper.update(qdesQstruct, updateWrapper);

        return map;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map uploadFileInfo(String id, MultipartFile file, boolean keepext) throws Exception {
        UploadFileInfo uploadFileInfo = new UploadFileInfo();

        //1:通过qstructId获取projectId,attachments
        QdesQstruct qdesQstructInfo = baseMapper.selectById(id);
        String projectId = qdesQstructInfo.getProjectId();
        String attachments = qdesQstructInfo.getAttachments();

        String filename = file.getOriginalFilename();
        String fileInfoId;
        String fileMapId;
        String url;

        if (!keepext) {
            filename = filename.substring(0, filename.lastIndexOf("."));
        }
        String fileId = DigestUtils.md5Hex(file.getInputStream());
        //创建查询条件对象
        LambdaQueryWrapper<UploadFileInfo> queryWrapper = Wrappers.lambdaQuery();
        //根据fileId判断图片是否上传过MD5
        queryWrapper
                .eq(UploadFileInfo::getFileId, fileId)
                .eq(UploadFileInfo::getDeleted, 0).last("limit 1");
        UploadFileInfo isExistUploadFileInfo = uploadFileInfoMapper.selectOne(queryWrapper);
        //1.插upload_fileinfo信息
        if (ObjectUtil.isEmpty(isExistUploadFileInfo)) {
            String path = ROOT_FILE_PATH + File.separator + StrUtil.replace(OssPathEnum.TEMP_QDES.getPath(), "/", File.separator);
            File file1 = new File(path);
            if (!file1.exists()) {
                file1.mkdirs();
            }
            File fileInfo = new File(path, filename);
            file.transferTo(fileInfo);
            //上传附件
            //url = ossService.uploadSimpleFile(fileInfo, fileId.substring(5), OssPathEnum.QDES, 0, null, "");
            url = storageService.upload(fileInfo, fileId.substring(5), OssPathEnum.QDES.getPath(), new Dict()).getUrl();
            uploadFileInfo.setFileId(fileId);
            uploadFileInfo.setFileSize((int) fileInfo.length());
            uploadFileInfo.setFtype(FileInfoFTypeEnum.FILEINFO_FTYPE_FILE.getValue());
            uploadFileInfo.setPolicy(1);
            uploadFileInfo.setUrl(url);
            uploadFileInfoMapper.insert(uploadFileInfo);
            fileInfoId = uploadFileInfo.getId();
            // 删除临时文件
            fileInfo.delete();
        } else {
            fileInfoId = isExistUploadFileInfo.getId();
            url = isExistUploadFileInfo.getUrl();
        }
        //2.插upload_filemap信息
        fileMapId = uploadFileMap(fileInfoId, projectId, filename);

        //3.更新qdesQstruct
        List<Object> list = JSONArray.parseArray(attachments, Object.class);
        Map<String, Object> map = new HashMap(16);
        map.put("id", fileMapId);
        map.put("fileName", filename);
        map.put("filePath", url);
        map.put("updated_dt", DateUtil.convertDteToString(DateUtil.getCommonDate(), ConditionDataConverter.DATETIME_FORMAT1));
        list.add(map);
        attachments = JSONObject.toJSONString(list);

        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setAttachments(attachments);
        //3.更新attachments
        baseMapper.update(qdesQstruct, updateWrapper);

        return map;
    }

    public String uploadFileMap(String fileInfoId, String projectId, String fileName) {
        UploadFileMap uploadFileMap = new UploadFileMap();
        uploadFileMap.setFileName(fileName);
        uploadFileMap.setFileInfoId(fileInfoId);
        uploadFileMap.setOriginFileInfoId(fileInfoId);
        uploadFileMap.setSysId("qdes");

        uploadFileMapMapper.insert(uploadFileMap);

        return uploadFileMap.getId();
    }

    @Override
    public List<QstructsAttachmentsVo> getAttachments(String id, String search) {
        List<QstructsAttachmentsVo> qstructsAttachmentsVoList = new ArrayList<>();
        String attachments = baseMapper.selectById(id).getAttachments();
        List<Object> list = JSONArray.parseArray(attachments, Object.class);
        for (int i = 0; i < list.size(); i++) {
            QstructsAttachmentsVo qstructsAttachmentsVo = new QstructsAttachmentsVo();
            Map<String, Object> map = (Map<String, Object>) list.get(i);
            if (StringUtils.isNotEmpty(search)) {
                if (((String) map.get("fileName")).contains(search)) {
                    qstructsAttachmentsVo.setId((String) map.get("id"));
                    qstructsAttachmentsVo.setFileName((String) map.get("fileName"));
                    qstructsAttachmentsVo.setFilePath((String) map.get("filePath"));
                    qstructsAttachmentsVo.setUpdatedDt((String) map.get("updated_dt"));
                    qstructsAttachmentsVoList.add(qstructsAttachmentsVo);
                }
            } else {
                qstructsAttachmentsVo.setId((String) map.get("id"));
                qstructsAttachmentsVo.setFileName((String) map.get("fileName"));
                qstructsAttachmentsVo.setFilePath((String) map.get("filePath"));
                qstructsAttachmentsVo.setUpdatedDt((String) map.get("updated_dt"));
                qstructsAttachmentsVoList.add(qstructsAttachmentsVo);
            }

        }
        return qstructsAttachmentsVoList;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DeletedCountVo deleteAttachments(String id, String tid) {
        DeletedCountVo deletedCountVo = new DeletedCountVo();
        String attachments = baseMapper.selectById(id).getAttachments();
        List<Object> list = JSONArray.parseArray(attachments, Object.class);
        List<Object> tempList = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            Map<String, Object> map;
            map = (Map<String, Object>) list.get(i);
            if (!map.get("id").equals(tid)) {
                tempList.add(map);
            }
        }
        attachments = JSONObject.toJSONString(tempList);
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setAttachments(attachments);
        //更新templates
        int count = baseMapper.update(qdesQstruct, updateWrapper);
        deletedCountVo.setDeletedCount(count);

        //删除upload_filemap,先查询对应的fileinfoId，后续判断是否删除fileinfo
        UploadFileMap uploadFileMap = uploadFileMapMapper.selectById(tid);
        //创建修改条件对象
        LambdaQueryWrapper<UploadFileMap> queryWrapper = new LambdaQueryWrapper<>();
        //构造查询条件
        queryWrapper
                .eq(UploadFileMap::getFileInfoId, uploadFileMap.getFileInfoId());
        long countFileMap = uploadFileMapMapper.selectCount(queryWrapper);

        uploadFileMapMapper.deleteById(tid);
        if (countFileMap == 1) {
            //需要删除upload_fileinfo
            uploadFileInfoMapper.deleteById(uploadFileMap.getFileInfoId());
        }

        return deletedCountVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String putScores(String id, cn.hutool.json.JSONObject scoresJson) {
        //1:通过qstructId获取scores
        QdesQstruct qstruct = baseMapper.selectById(id);
        String scores = qstruct.getScores();
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        if ("{}".equals(scores)) {
            //创建修改条件对象
            qdesQstruct.setScores(scoresJson.toString());
        } else {
            JSONObject scoresQstruct = JSONObject.parseObject(scores);
            //1.先把前端传的json放进map
            Map<String, Object> userMap = new HashMap<>(scoresJson);
            //2.保留系统中在参数中没有的key
            for (Map.Entry<String, Object> map : scoresQstruct.entrySet()) {
                if (!userMap.containsKey(map.getKey())) {
                    userMap.put(map.getKey(), map.getValue());
                }
            }
            qdesQstruct.setScores(JSON.toJSONString(userMap));
        }

        QdesProject proj = qdesProjectMapper.selectById(qstruct.getProjectId());
        if (ObjectUtil.isNotEmpty(proj.getReleaseVer())) {
            QdesQstruct lastQstruct = qdesProjectService.getQstructByVersion(proj.getId(), proj.getReleaseVer());
            //QdesQstruct lastQstruct = this.getQstructByVersion(proj.getId(), proj.getReleaseVer());
            if (!StrUtil.equals(lastQstruct.getScores(), qdesQstruct.getScores())) {
                qdesQstruct.setStatus(QSTRUCT_STATUS_DRAFT.getNumber());
            }
        }
        baseMapper.update(qdesQstruct, updateWrapper);

        return id;
    }

    @Override
    public Map getScores(String id) {
        //1:通过qstructId获取scores
        String scores = baseMapper.selectById(id).getScores();
        JSONObject scoresQstruct = JSONObject.parseObject(scores);
        Map<String, Object> scoresMap = new HashMap<>();
        for (Map.Entry<String, Object> scoresJsonentry : scoresQstruct.entrySet()) {
            scoresMap.put(scoresJsonentry.getKey(), scoresJsonentry.getValue());
        }
        return scoresMap;
    }

    @Override
    public Map getScoreConfig(String id) {
        //1:通过qstructId获取scoresConfig
        String scoresConfig = baseMapper.selectById(id).getScoreConfig();
        JSONObject scoresQstruct = JSONObject.parseObject(scoresConfig);
        Map<String, Object> scoresConfigMap = new HashMap<>();
        for (Map.Entry<String, Object> scoresJsonentry : scoresQstruct.entrySet()) {
            scoresConfigMap.put(scoresJsonentry.getKey(), scoresJsonentry.getValue());
        }
        return scoresConfigMap;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String putScoreConfig(String id, cn.hutool.json.JSONObject scoresJson) {
        //1:通过qstructId获取scoresConfig
        String scoresConfig = baseMapper.selectById(id).getScoreConfig();
        JSONObject scoresQstruct = JSONObject.parseObject(scoresConfig);
        //1.先把前端传的json放进map
        Map<String, Object> userMap = new HashMap<>(scoresJson);
        //2.保留系统中在参数中没有的key
        for (Map.Entry<String, Object> map : scoresQstruct.entrySet()) {
            if (!userMap.containsKey(map.getKey())) {
                userMap.put(map.getKey(), map.getValue());
            }
        }
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setScoreConfig(JSON.toJSONString(userMap));
        //更新scores
        baseMapper.update(qdesQstruct, updateWrapper);
        return id;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String putOutward(String id, Map outward) {
        Object data = outward.get("data");
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setData(JSON.toJSONString(data));
        //更新scores
        baseMapper.update(qdesQstruct, updateWrapper);
        return id;
    }

    @Override
    public BigDecimal getMaxScores(String id) {
        //1:通过qstructId获取scores
        Double maxScore = Double.valueOf(0);
        String scores = baseMapper.selectById(id).getScores();
        JSONObject scoresQstruct = JSONObject.parseObject(scores);
        for (Map.Entry<String, Object> scoresJsonentry : scoresQstruct.entrySet()) {
            LambdaQueryWrapper<QdesQuestion> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(QdesQuestion::getStructId, id)
                    .eq(QdesQuestion::getGid, scoresJsonentry.getKey())
                    .last("limit 1");
            //多选题每个选项分数累加，单选题统计最大值
            QdesQuestion qdesQuestion = qdesQuestionMapper.selectOne(wrapper);
            String data = qdesQuestion.getData();
            List<JSONObject> list = JSONArray.parseArray(JSONObject.toJSONString(scoresJsonentry.getValue(), SerializerFeature.WriteClassName), JSONObject.class);
            if ("multiple".equals(JSONObject.parseObject(data).getString("qtype")) ||
                    "matrix_multiple".equals(JSONObject.parseObject(data).getString("qtype"))) {
                //多选 矩阵多选 每个评分累加
                for (int i = 0; i < list.size(); i++) {
                    Map map = (Map) JSON.parse(String.valueOf(list.get(i)));
                    maxScore += ObjectUtils.isEmpty(map.get("score")) ? 0 : Double.parseDouble((String) map.get("score"));
                }
            } else if ("matrix_single".equals(JSONObject.parseObject(data).getString("qtype"))) {
                //矩阵单选，取每个矩阵行最大评分相加
                List<Map> mapList = new ArrayList<>();
                for (int i = 0; i < list.size(); i++) {
                    Map map = (Map) JSON.parse(String.valueOf(list.get(i)));
                    String rgid = (String) map.get("r_gid");
                    String score = (String) map.get("score");
                    Map maptemp = new HashMap<>();
                    maptemp.put(rgid, score);
                    mapList.add(maptemp);
                }
                JSONArray ary = JSONArray.parseArray(JSON.toJSONString(mapList));
                Map<String, Object> retmap = new HashMap<>();
                ary.forEach(k -> {
                    JSONObject obj = (JSONObject) k;
                    Map<String, Object> map = obj.getInnerMap();
                    map.forEach((j, l) -> {
                        if (!retmap.containsKey(j)) {
                            retmap.put(j, l);
                        } else {
                            Double newval = Double.parseDouble(l.toString());
                            Double oldval = Double.parseDouble(retmap.getOrDefault(j, 0).toString());
                            if (newval > oldval) {
                                retmap.put(j, l);
                            }
                        }
                    });
                });

                for (Map.Entry<String, Object> entry : retmap.entrySet()) {
                    maxScore += Double.parseDouble((String) entry.getValue());
                }

            } else {
                //单选 取选项最大评分
                Set<Double> score = new HashSet<>();
                for (int i = 0; i < list.size(); i++) {
                    Map map = (Map) JSON.parse(String.valueOf(list.get(i)));
                    Double b = ObjectUtils.isEmpty(map.get("score")) ? 0 : Double.parseDouble((String) map.get("score"));
                    score.add(b);
                }
                maxScore += getMaxScore(score);

            }
        }
        //前端控制了只保留2位，这里无需处理
        return BigDecimal.valueOf(maxScore).setScale(2, RoundingMode.HALF_UP);
    }

    private Double getMaxScore(Set<Double> score) {
        return score.stream().max(Double::compare).orElse(0.0);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Object> postPolicys(String id, List<Map> policyParam) {
        List<Object> policyMap = new ArrayList<>();
        String policy = baseMapper.selectById(id).getPolicy();
        List<Object> list = JSONArray.parseArray(policy, Object.class);
        if (list.size() == 0) {
            for (int i = 0; i < policyParam.size(); i++) {
                Map map = new HashMap();
                map.put("name", policyParam.get(i).get("name"));
                map.put("file_path", "");
                map.put("filemap_id", "");
                policyMap.add(map);
            }
            //创建修改条件对象
            LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
            //构造查询条件
            updateWrapper
                    .eq(QdesQstruct::getId, id);
            QdesQstruct qdesQstruct = new QdesQstruct();
            qdesQstruct.setPolicy(JSON.toJSONString(policyMap));
            //更新policy
            baseMapper.update(qdesQstruct, updateWrapper);
        } else {
            return list;
        }
        return policyMap;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map putPolicys(String id, Map policyParam) throws Exception {
        Map returnMap = new HashMap<>();
        String fileInfoId;
        String url;
        List<Object> policyMap = new ArrayList<>();
        QdesQstruct qdesQstructInfo = baseMapper.selectById(id);
        String projectId = qdesQstructInfo.getProjectId();
        String policy = qdesQstructInfo.getPolicy();
        List<Object> list = JSONArray.parseArray(policy, Object.class);
        for (int i = 0; i < list.size(); i++) {
            Map map = (Map) list.get(i);
            if (map.get("name").equals(policyParam.get("name"))) {
                if (ObjectUtils.isEmpty(policyParam.get("content"))) {
                    map.put("file_path", "");
                    map.put("filemap_id", "");
                    policyMap.add(map);
                    returnMap.put("file_path", "");
                } else {
                    //1.插入fileinfo信息
                    String content = (String) policyParam.get("content");
                    String fileId = DigestUtils.md5Hex(content);
                    //创建查询条件对象
                    LambdaQueryWrapper<UploadFileInfo> queryWrapper = Wrappers.lambdaQuery();
                    //根据fileId判断文件是否上传过MD5
                    queryWrapper
                            .eq(UploadFileInfo::getFileId, fileId)
                            .eq(UploadFileInfo::getDeleted, 0).last("limit 1");
                    UploadFileInfo isExistUploadFileInfo = uploadFileInfoMapper.selectOne(queryWrapper);
                    String filename = fileId.substring(5);
                    //1.插upload_fileinfo信息
                    if (ObjectUtil.isEmpty(isExistUploadFileInfo)) {
                        FileOutputStream out = null;
                        try {
                            //上传文件
                            String path = ROOT_FILE_PATH + File.separator + StrUtil.replace(OssPathEnum.TEMP_QDES.getPath(), "/", File.separator);
                            File file1 = new File(path);
                            if (!file1.exists()) {
                                file1.mkdirs();
                            }
                            File fileInfo = new File(path, filename);
                            out = new FileOutputStream(fileInfo);
                            byte[] contentBytes = content.getBytes();
                            out.write(contentBytes);
                            //上传文件
                            //url = ossService.uploadSimpleFile(fileInfo, filename, OssPathEnum.QDES, 0, null, "");
                            url = storageService.upload(fileInfo, filename, OssPathEnum.QDES.getPath(), new Dict()).getUrl();
                            UploadFileInfo uploadFileInfo = new UploadFileInfo();
                            uploadFileInfo.setFileId(fileId);
                            uploadFileInfo.setFileSize((int) fileInfo.length());
                            uploadFileInfo.setFtype(FileInfoFTypeEnum.FILEINFO_FTYPE_FILE.getValue());
                            uploadFileInfo.setPolicy(1);
                            uploadFileInfo.setUrl(url);
                            uploadFileInfoMapper.insert(uploadFileInfo);
                            fileInfoId = uploadFileInfo.getId();
                            //删除临时文件
                            fileInfo.delete();

                        } catch (Exception e) {
                            throw e;
                        } finally {
                            if (out != null) {
                                out.close();
                            }
                        }
                    } else {
                        fileInfoId = isExistUploadFileInfo.getId();
                        url = isExistUploadFileInfo.getUrl();
                    }
                    //2.插入filemap
                    UploadFileMap uploadFileMap = new UploadFileMap();
                    //uploadFileMap.setProjectId(projectId);
                    uploadFileMap.setFileInfoId(fileInfoId);
                    uploadFileMap.setOriginFileInfoId(fileInfoId);
                    uploadFileMap.setSysId("qdes");
                    uploadFileMap.setFileName(filename);
                    uploadFileMapMapper.insert(uploadFileMap);

                    map.put("file_path", url);
                    map.put("filemap_id", uploadFileMap.getId());
                    policyMap.add(map);
                    returnMap.put("file_path", url);
                }
            } else {
                policyMap.add(map);
            }
        }
        //创建修改条件对象
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件
        updateWrapper
                .eq(QdesQstruct::getId, id);
        QdesQstruct qdesQstruct = new QdesQstruct();
        qdesQstruct.setPolicy(JSON.toJSONString(policyMap));
        //更新policy
        baseMapper.update(qdesQstruct, updateWrapper);
        return returnMap;
    }

    @Override
    public List<QstructsDetailVo> getPublishRecord(GetPublishRecordParam param) {
        LambdaQueryWrapper<QdesQstruct> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Objects.nonNull(param.getProjectId()), QdesQstruct::getProjectId, param.getProjectId());
        if (Objects.nonNull(param.getRelease()) && param.getRelease().equals(1)) {
            wrapper.isNotNull(QdesQstruct::getReleaseDt);
        }

        String sortByColumn = param.getSortBy();
        if (StringUtils.isNotEmpty(sortByColumn)) {
            SFunction<QdesQstruct, ?> column = "version".equals(sortByColumn) ? QdesQstruct::getVersion : null;
            if (Objects.nonNull(param.getDescending()) && param.getDescending()) {
                wrapper.orderByDesc(column);
            } else {
                wrapper.orderByAsc(column);
            }
        }
        List<QdesQstruct> qdesQstructList = baseMapper.selectList(wrapper);
        return qstructConvertMapper.qstruct2Vo(qdesQstructList);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void importStructs(String id, ImportStructParam param) {
        List<String> importedIds = new ArrayList<>();
        try {
            QdesProject sourceProject = qdesProjectMapper.selectById(param.getSrcProjectId());
            QdesQstruct sourceQdesQstruct = qdesQstructMapper.selectById(sourceProject.getStructId());
            String structId = sourceQdesQstruct.getId();
            // 获取对应的问题
            LambdaQueryWrapper<QdesQuestion> queryWrapper = Wrappers.lambdaQuery();
            //根据fileId判断文件是否上传过MD5
            queryWrapper.eq(QdesQuestion::getStructId, structId)
                    .in(QdesQuestion::getGid, param.getSrcGids());
            List<QdesQuestion> dbSourceQuestions = qdesQuestionMapper.selectList(queryWrapper);
            // 按照src_gid重新排序
            List<QdesQuestion> sourceQuestions = new ArrayList<>();
            for (String gid : param.getSrcGids()) {
                for (QdesQuestion question : dbSourceQuestions) {
                    if (StringUtils.isNotEmpty(question.getData())) {
                        JSONObject dataJs = JSONObject.parseObject(question.getData());
                        if (gid.equals(dataJs.getString("gid"))) {
                            sourceQuestions.add(question);
                            break;
                        }
                    }
                }
            }
            // 去掉问题里面旋相关联字段
            for (QdesQuestion question : sourceQuestions) {
                if (StringUtils.isNotEmpty(question.getData())) {
                    JSONObject dataJs = JSONObject.parseObject(question.getData());
                    if (dataJs.getJSONArray("items") != null) {
                        JSONArray array = dataJs.getJSONArray("items");
                        for (int i = 0; i < array.size(); i++) {
                            JSONObject item = array.getJSONObject(i);
                            if (item.containsKey("option_origin")) {
                                item.remove("option_origin");
                            }
                        }
                        question.setData(dataJs.toJSONString());
                    }
                }
            }
            QdesProject targetProject = qdesProjectMapper.selectById(id);
            if (targetProject == null) {
                throw new BusinessException("The target project is null, id = " + id);
            }
            QdesQstruct targetQdesQstruct = qdesQstructMapper.selectById(targetProject.getStructId());
            if (targetQdesQstruct == null) {
                throw new BusinessException("The target qstruct is null, id = " + targetProject.getStructId());
            }
            String targetStructId = targetQdesQstruct.getId();
            // 获取对应的问题
            LambdaQueryWrapper<QdesQuestion> targetStructIdQueryWrapper = Wrappers.lambdaQuery();
            //根据fileId判断文件是否上传过MD5
            targetStructIdQueryWrapper.eq(QdesQuestion::getStructId, targetStructId);
            List<QdesQuestion> targetQuestions = qdesQuestionMapper.selectList(targetStructIdQueryWrapper);

            String originalQstructData = targetQdesQstruct.getData();
            // 获取gids数量
            int dispatchCounts = getDispatchCounts(sourceQuestions);
            // 获取最大gid、分组信息、获取结构表中插入的那条问题的index和所在分组的groupid
            JSONObject resdic = getInfo(targetQdesQstruct, targetProject, dispatchCounts, null);
            int page = resdic.getInteger("page");
            String gids = resdic.getString("dispatch_gids");
            log.info("question_import_handler_dispatch_gids = {}", gids);

            JSONObject quotaQdict = new JSONObject();
            quotaQdict.put("src_project_id", sourceProject.getId());
            quotaQdict.put("target_project_id", targetProject.getId());
            quotaQdict.put("qgid_map", "{}");
            quotaQdict.put("qcid_map", "{}");
            quotaQdict.put("ogid_map", "{}");
            quotaQdict.put("qgid_list", "[]");
            quotaQdict.put("qtitle_map", "{}");
            JSONObject allQdict = new JSONObject(); // 所有源、目标题目gid对应关系，包含选项
            List<JSONObject> preparedQuestions = new ArrayList<>(); // 修改gid和cid后的题目列表
            JSONObject qcidQgidDict = new JSONObject();
            List<String> newCids = new ArrayList<>(); // 新生成的问题cid，用于检测cid冲突

            // 获取目标所有题目中最大index
            int targetMaxIndex = getMaxIndex(targetQdesQstruct);
            // gids list下标
            int gidIndex = resdic.getInteger("dispatch_gid_index");
            dispatchGid(sourceQuestions, targetQuestions, gids, quotaQdict, allQdict, newCids, qcidQgidDict, targetMaxIndex, gidIndex);
            log.info("question_import_handler_match_gids = {}", allQdict);

            // 选项关联
            // await self.option_relate(source_qstruct, src_gids, all_qdict, target_qstruct)
            // 题目库模板标签 作为 题目标签
            List<JSONObject> tagList = new ArrayList<>();
            if (sourceProject.getPtype() == PROJECT_PTYPE_QUESITON.getNumber()) {
                LambdaQueryWrapper<QdesQuestionLib> qWrapper = Wrappers.lambdaQuery();
                //根据fileId判断文件是否上传过MD5
                qWrapper.eq(QdesQuestionLib::getProjectId, param.getSrcProjectId()).last("limit 1");
                QdesQuestionLib qLib = qdesQuestionlibMapper.selectOne(qWrapper);
                if (qLib != null) {
                    LambdaQueryWrapper<QdesTagRef> refwrapper = new LambdaQueryWrapper();
                    refwrapper.eq(QdesTagRef::getRefId, qLib.getId()).eq(QdesTagRef::getRefType, TAGREF_REF_TYPE_LIBRARY);
                    List<QdesTagRef> tagrefs = qdesTagrefMapper.selectList(refwrapper);
                    for (QdesTagRef tagref : tagrefs) {
                        JSONObject tagJson = new JSONObject();
                        tagJson.put("tagid", tagref.getTagId());
                        tagJson.put("tag_title", tagref.getTagTitle());
                        tagList.add(tagJson);
                    }
                }
            }
            // 最终题目赋值
            for (QdesQuestion question : sourceQuestions) {
                // 检查是否设置单元格不可填写
                JSONObject sourceQuestionData = JSONObject.parseObject(question.getData());
                sourceQuestionData = sourceQuestionData == null ? new JSONObject() : sourceQuestionData;
                JSONObject qDict = cellDisabled(sourceQuestionData, allQdict);
                question.setData(sourceQuestionData.toJSONString());
                if (!CollectionUtils.isEmpty(tagList)) {
                    qDict.put("assignTags", tagList);
                }
                // 附件选项排出
                JSONObject attachDatajs = qDict.getJSONObject("attachData") == null ? new JSONObject() : qDict.getJSONObject("attachData");
                String excludeOptionStr = attachDatajs.getString("excludeOptionList");
                List<String> excludeOptionList = JSONObject.parseArray(excludeOptionStr, String.class);
                excludeOptionList = excludeOptionList == null ? new ArrayList<>() : excludeOptionList;
                List<String> newExcludeOptionList = new ArrayList<>();
                excludeOptionList.forEach(exlOp -> {
                    String newExlOp = allQdict.getString(exlOp);
                    if (StringUtils.isNotEmpty(newExlOp)) {
                        newExcludeOptionList.add(newExlOp);
                    }
                });
                if (CollectionUtils.isNotEmpty(newExcludeOptionList)) {
                    attachDatajs.put("excludeOptionList", newExcludeOptionList);
                    qDict.put("attachData", attachDatajs);
                }
                preparedQuestions.add(qDict);
            }
            // 插入结构表items[]的数据
            List<JSONObject> insertItemQuestions = new ArrayList<>();
            List<JSONObject> insertGroupQuestions = new ArrayList<>();
            preparedQuestions.forEach(question -> {
                if (resdic.getBoolean("isgroup")) {
                    JSONObject newItemQuestion = new JSONObject();
                    newItemQuestion.put("_index", question.get("_index"));
                    newItemQuestion.put("pGroupGid", resdic.get("groupid"));
                    newItemQuestion.put("disp_code", question.getOrDefault("disp_code", ""));
                    newItemQuestion.put("gid", question.get("gid"));
                    insertItemQuestions.add(newItemQuestion);
                    JSONObject newGroupQuestion = new JSONObject();
                    newGroupQuestion.put("type", "Q");
                    newGroupQuestion.put("gid", question.get("gid"));
                    insertGroupQuestions.add(newGroupQuestion);
                } else {
                    JSONObject newItemQuestion = new JSONObject();
                    newItemQuestion.put("_index", question.get("_index"));
                    newItemQuestion.put("gid", question.get("gid"));
                    insertItemQuestions.add(newItemQuestion);
                }
            });
            // 如果是分组模式，插入struct.data["group"][key]["sorts"]
            groupInsertSorts(resdic, targetQdesQstruct, null, insertGroupQuestions);
            // 不管是不是分组模式，都要插入到结构表items[] judge_id后面
            Integer judgeIndex = resdic.getInteger("index");
            allInsertItems(targetQdesQstruct, page, judgeIndex, insertItemQuestions);
            // 检查题目跳转
            questionJump(sourceQdesQstruct, param.getSrcGids(), allQdict, targetQdesQstruct);
            // 检查题目显示逻辑
            questionDisplay(sourceQdesQstruct, param.getSrcGids(), allQdict, targetQdesQstruct);
            // 检查答案引用，获取gid和题目title关系，引用的题目如果不存在需要修改题目title
            answerRef(sourceQdesQstruct, param.getSrcGids(), allQdict, targetQdesQstruct);
            // 检查引用顺序
            optionOrder(sourceQdesQstruct, param.getSrcGids(), allQdict, targetQdesQstruct);
            // 创建题目
            List<QdesQuestion> questions = new ArrayList<>();
            for (JSONObject params : preparedQuestions) {
                Integer gid = params.getInteger("gid");
                QdesQuestion question = new QdesQuestion();
                question.setStructId(targetStructId);
                question.setGid(gid);
                question.setData(params.toJSONString());
                question.setModified(false);
                questions.add(question);
            }
            if (CollectionUtils.isNotEmpty(questions)) {
                qdesQuestionMapper.insertBatchSomeColumn(questions);
                questions.forEach(question -> {
                    importedIds.add(question.getId());
                });
            }
            // 更新结构表
            JSONObject params = new JSONObject();
            params.put("data", targetQdesQstruct.getData());
            updateQstruct(targetQdesQstruct, params, targetProject, false, false);
            // 复制交叉配额方法
            copyQuotasByGids(quotaQdict);
        } catch (Exception exp) {
            log.error("server update failed, e = ", exp);
            throw new BusinessException("server update failed");
        }
    }

    @Override
    public QdesProjectVo projectQStructImport(String id, ProjectCreateParam param) {
        log.info("Now to projectQStructImport, id = {}", id);
        String userId = param.getUserId();
        String orgId = param.getOrgId();
        QdesQstruct qstruct = qdesQstructMapper.selectById(id);
        if (qstruct == null) {
            throw new BusinessException("qstruct id is invaild");
        }
        String projectId = qstruct.getProjectId();
        QdesProject project = qdesProjectMapper.selectById(projectId);
        if (!userId.equals(project.getCreator()) && !orgId.equals(project.getCompanyId())) {
            throw new BusinessException("qstruct id is not match");
        }
        String permissions = JSONObject.toJSONString(Arrays.stream(ProjectPermissionEnum.values())
                .map(ProjectPermissionEnum::getValue).collect(Collectors.toList()));
        // python有查看限额，已经不用了
        // 超级管理员和高级管理员可以看见所有项目
        List<String> adminIds = userGrpcClient.listOrgAdminIds(orgId);
        List<String> seniorIds = userGrpcClient.getSeniorAdminIdByOrgId(orgId);
        List<String> allUsers = new ArrayList<>();
        allUsers.add(userId);
        if (CollectionUtils.isNotEmpty(adminIds)) {
            allUsers.addAll(adminIds);
        }
        if (CollectionUtils.isNotEmpty(seniorIds)) {
            allUsers.addAll(seniorIds);
        }
        Set<String> userSet = new HashSet<>(allUsers);
        userSet.forEach(oneId ->
        {
            QdesProjectEntry projectentry = new QdesProjectEntry();
            projectentry.setOrgId(orgId);
            projectentry.setProjectId(project.getId());
            projectentry.setCategory(PROJECTENTRY_CATEGORY_MANAGER.getNumber());
            projectentry.setPermissionsArry(permissions);
            projectentry.setUserId(oneId);
            projectentry.setTitle(param.getTitle());
            projectentry.setStatus(ProjectEntryStatus.PROJECTENTRY_STATUS_CREATED.getNumber());
            projectentry.setCreatorId(userId);
            projectentry.setPermType(PROJECTENTRY_PERM_TYPE_CUSTOM.getNumber());
            if (StringUtils.isNotEmpty(param.getFolderId())) {
                projectentry.setFolderId(param.getFolderId());
            } else {
                projectentry.setFolderId("");
            }
            projectentry.setBadgeStatus(PROJECTENTRY_BADGE_STATUS_READ.getValue());
            qdesProjectentryMapper.insert(projectentry);
        });
        //创建修改条件对象
        LambdaUpdateWrapper<QdesProject> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(QdesProject::getTitle, param.getTitle())
                .set(QdesProject::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProject::getId, project.getId());
        String attr = project.getCustomAttr();
        if (StringUtils.isNotEmpty(attr)) {
            JSONObject attJs = JSONObject.parseObject(attr);
            attJs.put("fstatus", "down");
            updateWrapper.set(QdesProject::getCustomAttr, attJs.toJSONString());
        }
        qdesProjectMapper.update(null, updateWrapper);
        CreateProjectDto projectDto = new CreateProjectDto().setId(project.getId()).setOrgId(orgId);
        qdesMessageSendService.qdesProjectCreatedSend(projectDto);
        QdesProjectVo projectVo = qdesProjectConvertMapper.project2Vo(project);
        projectVo.setFolderId(param.getFolderId());
        projectVo.setOrgId(param.getOrgId());
        projectVo.setTitle(param.getTitle());
        return projectVo;
    }

    private void copyQuotasByGids(JSONObject data) {
        // 跨问卷复制配额
        if (data == null || data.isEmpty()) {
            return;
        }
        String srcProjectId = data.getString("src_project_id");
        String targetProjectId = data.getString("target_project_id");
        JSONObject qgidMap = data.getJSONObject("qgid_map");
        JSONObject qcidMap = data.getJSONObject("qcid_map");
        JSONObject ogidMap = data.getJSONObject("ogid_map");
        if (StringUtils.isEmpty(srcProjectId) || StringUtils.isEmpty(targetProjectId) || qgidMap == null || qgidMap.isEmpty()) {
            return;
        }
        // 查询需要复制的quotacondition，不存在就return
        List<String> qgids = new ArrayList<>(qgidMap.keySet());
        // 查询需要复制的 quota、quotacheckitems
        List<String> quotaIdList = surveyGrpcClient.getQuotaIdList(srcProjectId, qgids);
        if (CollectionUtils.isEmpty(quotaIdList)) {
            return;
        }
        LambdaQueryWrapper<QdesQuota> quotaWrapper = new LambdaQueryWrapper<>();
        quotaWrapper.in(QdesQuota::getId, quotaIdList);
        List<QdesQuota> quotas = qdesQuotaMapper.selectList(quotaWrapper);
        // 复制配额
        List<String> allQuotaIds = new ArrayList<>();
        for (QdesQuota quota : quotas) {
            // 插入QdesQuota
            QdesQuota qdesQuota = new QdesQuota();
            qdesQuota.setName(quota.getName());
            qdesQuota.setProjectId(targetProjectId);
            qdesQuota.setQuotaType(quota.getQuotaType());
            qdesQuota.setAvaQgidList(regenAvaQgidList(quota.getAvaQgidList(), qgidMap, qcidMap));
            qdesQuota.setRow1(regenRows(quota.getRow1(), qgidMap, ogidMap));
            qdesQuota.setRow2(regenRows(quota.getRow2(), qgidMap, ogidMap));
            qdesQuota.setCol1(regenRows(quota.getCol1(), qgidMap, ogidMap));
            qdesQuota.setCol2(regenRows(quota.getCol2(), qgidMap, ogidMap));
            qdesQuota.setQuotaCondList("[]");
            qdesQuotaMapper.insert(qdesQuota);
            String newQuotaId = qdesQuota.getId();
            allQuotaIds.add(newQuotaId);
            List<String> newQuotaCondIds = new ArrayList<>();
            String quotaCondList = quota.getQuotaCondList();
            if (StringUtils.isNotEmpty(quotaCondList)) {
                List<String> conditionList = JSONArray.parseArray(quotaCondList, String.class);
                if (!CollectionUtils.isEmpty(conditionList)) {
                    // 调用grpc
                    surveyGrpcClient.copyImportSurveyQuotas(conditionList, targetProjectId, newQuotaId, JSONObject.toJSONString(data));
                    if (CollectionUtils.isNotEmpty(newQuotaCondIds)) {
                        LambdaUpdateWrapper<QdesQuota> updateWrapper = new LambdaUpdateWrapper<>();
                        //构造查询条件
                        updateWrapper.set(QdesQuota::getQuotaCondList, newQuotaCondIds)
                                .set(QdesQuota::getUpdatedDt, LocalDateTime.now())
                                .eq(QdesQuota::getId, newQuotaId);
                        qdesQuotaMapper.update(null, updateWrapper);
                    }
                }
            }
        }
    }

    private String regenAvaQgidList(String avaQgidList, JSONObject qgidMap, JSONObject qcidMap) {
        // 重新生成 ava_qgid_list，普通配额为[]，交叉配额有值
        if (StringUtils.isEmpty(avaQgidList)) {
            return avaQgidList;
        }
        List<JSONObject> avaList = JSONObject.parseArray(avaQgidList, JSONObject.class);
        for (JSONObject ava : avaList) {
            String targetGid = Optional.ofNullable(qgidMap.getString(ava.getString("gid"))).orElse("");
            String targetCid = Optional.ofNullable(qcidMap.getString(ava.getString("cid"))).orElse("");
            ava.put("gid", Optional.ofNullable(qgidMap.getString(ava.getString("gid"))).orElse(""));
            ava.put("cid", targetCid);
            String[] titleArr = ava.getString("title").split("\\.");
            List<String> titles = Arrays.asList(titleArr);
            titles.set(0, targetCid);
            ava.put("title", String.join(".", titles));
            List<JSONObject> children = JSONObject.parseArray(ava.getString("children"), JSONObject.class);
            children = children == null ? new ArrayList<>() : children;
            children.forEach(option -> {
                option.put("pGid", targetGid);
                List<String> composeGid = JSONObject.parseArray(option.getString("composeGid"), String.class);
                if (CollectionUtils.isNotEmpty(composeGid)) {
                    List<String> composeGidNew = new ArrayList<>();
                    composeGid.forEach(compose -> {
                        String[] composeList = compose.split("_");
                        String firstComp = composeList[0];
                        String secondComp = composeList.length > 1 ? composeList[1] : "";
                        List<String> cList = new ArrayList<>();
                        cList.add(Optional.ofNullable(qgidMap.getString(firstComp)).orElse(""));
                        cList.add(Optional.ofNullable(qgidMap.getString(secondComp)).orElse(""));
                        composeGidNew.add(String.join("_", cList));
                    });
                    option.put("composeGid", composeGidNew);
                    option.put("gid", String.join(",", composeGidNew));
                } else {
                    List<String> tmpList = option.getString("gid") == null ? new ArrayList<>()
                            : Arrays.asList(option.getString("gid").split("_"));
                    String firstComp = tmpList.size() > 0 ? tmpList.get(0) : "";
                    String secondComp = tmpList.size() > 1 ? tmpList.get(1) : "";
                    List<String> cList = new ArrayList<>();
                    cList.add(Optional.ofNullable(qgidMap.getString(firstComp)).orElse(""));
                    cList.add(Optional.ofNullable(qgidMap.getString(secondComp)).orElse(""));
                    option.put("gid", String.join(",", cList));
                }
            });
            ava.put("children", children);
        }
        return JSONObject.toJSONString(avaList);
    }

    private String regenRows(String row, JSONObject qgidMap, JSONObject ogidMap) {
        // 重新生成 row，普通配额为[]，交叉配额有值
        if (StringUtils.isEmpty(row)) {
            return row;
        }
        List<JSONObject> rowList = JSONObject.parseArray(row, JSONObject.class);
        for (JSONObject obj : rowList) {
            String targetGid = Optional.ofNullable(qgidMap.getString(obj.getString("qgid"))).orElse("");
            obj.put("qgid", targetGid);
            List<String> tmpList = obj.getString("ogid") == null ? new ArrayList<>()
                    : Arrays.asList(obj.getString("ogid").split("_"));
            String sec = tmpList.size() > 1 ? tmpList.get(1) : "";
            obj.put("ogid", targetGid + "_" + ogidMap.getString(sec));
        }
        return JSONObject.toJSONString(rowList);
    }

    private List<String> regeQgidList(List<String> qgidList, JSONObject qgidMap) {
        // 重新生成 row，普通配额为[]，交叉配额有值
        List<String> newQgidList = new ArrayList<>();
        if (CollectionUtils.isEmpty(qgidList)) {
            return newQgidList;
        }
        for (String qgid : qgidList) {
            String newQgid = qgidMap.getString(qgid);
            if (StringUtils.isNotEmpty(newQgid)) {
                newQgidList.add(newQgid);
            }
        }
        return newQgidList;
    }

    private String regenCheckpoints(List<String> qgidList, String checkpoints, JSONObject data, JSONObject qgidMap) {
        // 原来的配额检查点的题目假如在复制的题目中 则用原来的配额检查点，不然就新增配额检查点
        // 按照原来的题目顺序排序qgid_list，取最后一个题目生成配额检查点
        List<String> srcQgidList = data.getString("qgid_list") == null ? new ArrayList<>()
                : JSONObject.parseArray(data.getString("qgid_list"), String.class);
        // 原来的配额检查点的题目假如在复制的题目中 则用原来的配额检查点，不然就新增配额检查点
        List<String> checkpointIntersection = new ArrayList<>();
        List<JSONObject> checkpointList = StringUtils.isEmpty(checkpoints) ? new ArrayList<>()
                : JSONObject.parseArray(checkpoints, JSONObject.class);
        checkpointList.forEach(ckp -> {
            if (srcQgidList.contains(ckp.getString("gid"))) {
                checkpointIntersection.add(ckp.getString("gid"));
            }
        });
        if (CollectionUtils.isNotEmpty(checkpointIntersection)) {
            List<JSONObject> newCheckpoints = new ArrayList<>();
            for (String qgid : checkpointIntersection) {
                JSONObject qJs = new JSONObject();
                qJs.put("gid", qgidMap.getOrDefault(qgid, ""));
                qJs.put("cid", ((JSONObject) ((JSONObject) data.getOrDefault("qtitle_map", new JSONObject()))
                        .getOrDefault(qgid, new JSONObject())).get("cid"));
                qJs.put("title", parseHtmlStr(((JSONObject) ((JSONObject) data.getOrDefault("qtitle_map", new JSONObject()))
                        .getOrDefault(qgid, new JSONObject())).getString("title")));
                newCheckpoints.add(qJs);
            }
            return JSONObject.toJSONString(newCheckpoints);
        } else {
            // 配额中的最后一题
            // python中就是正序，不需要再排序
            List<JSONObject> newCheckpoints = new ArrayList<>();
            JSONObject qJs = new JSONObject();
            qJs.put("gid", qgidMap.getOrDefault(qgidList.get(qgidList.size() - 1), ""));
            qJs.put("cid", ((JSONObject) ((JSONObject) data.getOrDefault("qtitle_map", new JSONObject()))
                    .getOrDefault(qgidList.get(qgidList.size() - 1), new JSONObject())).get("cid"));
            qJs.put("title", parseHtmlStr(((JSONObject) ((JSONObject) data.getOrDefault("qtitle_map", new JSONObject()))
                    .getOrDefault(qgidList.get(qgidList.size() - 1), new JSONObject())).getString("title")));
            newCheckpoints.add(qJs);
            return JSONObject.toJSONString(newCheckpoints);
        }
    }

    private String regenOptionCombies(String optionCombiesStr, JSONObject qgidMap, JSONObject ogidMap) {
        List<String> optionCombies = StringUtils.isEmpty(optionCombiesStr) ? new ArrayList<>()
                : JSONObject.parseArray(optionCombiesStr, String.class);
        List<String> newOptionCombies = new ArrayList<>();
        optionCombies.forEach(optionCombie -> {
            List<String> options = Arrays.asList(optionCombie.split("\\|"));
            List<String> newOptionCombie = new ArrayList<>();
            options.forEach(option -> {
                String[] tmpList = option.split("_");
                String targetQgid = tmpList[0];
                String targetOgid = tmpList.length > 1 ? tmpList[1] : "";
                List<String> aList = new ArrayList<>();
                aList.add(Optional.ofNullable(qgidMap.getString(targetQgid)).orElse(""));
                aList.add(Optional.ofNullable(ogidMap.getString(targetOgid)).orElse(""));
                newOptionCombie.add(String.join("_", aList));
            });
            newOptionCombies.add(String.join("|", newOptionCombie));
        });
        return JSONObject.toJSONString(newOptionCombies);
    }

    private List<Integer> regenOptionList(List<Integer> optionList, JSONObject ogidMap) {
        List<Integer> newOgidList = new ArrayList<>();
        optionList.forEach(ogid -> {
            Integer newOgid = ogidMap.getInteger(String.valueOf(ogid));
            if (newOgid != null) {
                newOgidList.add(newOgid);
            }
        });
        return newOgidList;
    }

    // 解析html字符串
    private String parseHtmlStr(String htmlStr) {
        if (StringUtils.isEmpty(htmlStr)) {
            return "";
        }
        return HtmlUtil.cleanHtmlTag(htmlStr);
    }

    @Override
    public void updateQstruct(QdesQstruct qstruct, JSONObject params, QdesProject proj, boolean updateExt, boolean forTemplet) {
        String structId = qstruct.getId();
        if (StringUtils.isNotEmpty(proj.getSource()) && !updateExt) {
            throw new BusinessException("extend project, update is forbidden");
        }
        if (!proj.getVersion().equals(qstruct.getVersion())) {
            throw new BusinessException("qstruct is stale id = " + structId);
        }
        if (!PROJ_EDITABLE_STATUS_LIST.contains(proj.getStatus())) {
            throw new BusinessException("project.status is " + proj.getStatus() + ", update is forbidden");
        }
        if (params.getJSONObject("data") != null) {
            JSONObject scoreConfig = StringUtils.isEmpty(qstruct.getScoreConfig()) ? new JSONObject()
                    : JSONObject.parseObject(qstruct.getScoreConfig());
            JSONObject multipleQuestions = new JSONObject();
            JSONArray results = params.getJSONObject("data").getJSONArray("items");
            for (int i = 0; i < results.size(); i++) {
                JSONObject result = results.getJSONObject(i);
                JSONArray items = StringUtils.isEmpty(result.getString("items"))
                        ? new JSONArray() : result.getJSONArray("items");
                for (int j = 0; j < items.size(); j++) {
                    JSONObject item = items.getJSONObject(j);
                    if (item.containsKey("gid")) {
                        String scoreGidStr = item.getString("gid");
                        multipleQuestions.put(scoreGidStr, StringUtils.isEmpty(scoreConfig.getString(scoreGidStr))
                                ? "1" : scoreConfig.getString(scoreGidStr));
                    }
                }
            }
            params.put("score_config", multipleQuestions);
        }
        params.put("status", QSTRUCT_STATUS_DRAFT.getNumber());
        // 修改struct
        LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(QdesQstruct::getScoreConfig, params.getString("score_config"))
                .set(QdesQstruct::getStatus, params.getInteger("status"))
                .set(QdesQstruct::getUpdatedDt, LocalDateTime.now())
                .eq(QdesQstruct::getId, structId);
        if (params.containsKey("data")) {
            updateWrapper.set(QdesQstruct::getData, params.getString("data"));
        }
        baseMapper.update(null, updateWrapper);
        // 更新问卷时间
        LambdaUpdateWrapper<QdesProject> projectWrapper = new LambdaUpdateWrapper<>();
        projectWrapper.set(QdesProject::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProject::getId, proj.getId());
        qdesProjectMapper.update(null, projectWrapper);
        Map<Integer, JSONObject> allQids = new HashMap<>();

        LambdaQueryWrapper<QdesQuestion> questionWrapper = new LambdaQueryWrapper<>();
        questionWrapper.eq(QdesQuestion::getStructId, structId);
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        qdesQuestions.forEach(item -> {
            allQids.put(item.getGid(), JSONObject.parseObject(item.getData()));
        });
        // 前端修改题目会同时发两个put请求，修改question、修改qstruct
        // 当params['data'].items中不包含题目信息，此时不需要更新question
        JSONObject datajs = params.getJSONObject("data") == null ? new JSONObject() : params.getJSONObject("data");
        List<JSONObject> qsItems = new ArrayList<>();
        JSONArray reItems = params.getJSONObject("data").getJSONArray("items");
        if (reItems != null) {
            for (int i = 0; i < reItems.size(); i++) {
                qsItems.add(reItems.getJSONObject(i));
            }
        }

        qsItems.stream().findFirst()
                .map(first -> first.getJSONArray("items"))
                .filter(is -> is.size() > 2)
                .ifPresent(ret -> {
                    JSONArray questions = new JSONArray();
                    questionService.getQstructQuestions(datajs, allQids, questions);
                    Map<String, String> map = new HashMap<>();
                    map.put("projectId", qstruct.getProjectId());
                    map.put("version", qstruct.getVersion().toString());
                    map.put("secretKey", qstruct.getSecretKey());
                    questions.forEach(sob -> {
                        JSONObject dd = (JSONObject) sob;
                        questionService.genQuestionSign(map, dd.getString("gid"), dd);

                        QdesQuestion question = qdesQuestionMapper.selectOne(new LambdaQueryWrapper<QdesQuestion>()
                                .eq(QdesQuestion::getStructId, structId)
                                .eq(QdesQuestion::getGid, dd.getString("gid")).last("limit 1"));
                        if (question == null) {
                            question = new QdesQuestion();
                            question.setData(JSON.toJSONString(dd));
                            question.setStructId(structId);
                            question.setGid(dd.getInteger("gid"));
                            question.setModified(false);
                            qdesQuestionMapper.insert(question);
                        } else {
                            LambdaUpdateWrapper<QdesQuestion> upQuWrapper = new LambdaUpdateWrapper<>();
                            upQuWrapper.set(QdesQuestion::getData, JSON.toJSONString(dd))
                                    .eq(QdesQuestion::getStructId, structId)
                                    .eq(QdesQuestion::getGid, dd.getString("gid"));
                            qdesQuestionMapper.update(null, upQuWrapper);
                        }
                    });
                });

        /*if (CollectionUtils.isNotEmpty(qsItems) && qsItems.get(0).getJSONArray("items") != null
                && qsItems.get(0).getJSONArray("items").size() > 2) {

            JSONArray questions = new JSONArray();
            questionService.getQstructQuestions(datajs, allQids, questions);
            Map<String, String> map = new HashMap<>();
            map.put("projectId", qstruct.getProjectId());
            map.put("version", qstruct.getVersion().toString());
            map.put("secretKey", qstruct.getSecretKey());
            questions.forEach(sob -> {
                JSONObject dd = (JSONObject) sob;
                questionService.genQuestionSign(map, dd.getString("gid"), dd);

                LambdaUpdateWrapper<QdesQuestion> upQuWrapper = new LambdaUpdateWrapper<>();
                upQuWrapper.set(QdesQuestion::getData, JSON.toJSONString(dd))
                        .eq(QdesQuestion::getStructId, structId)
                        .eq(QdesQuestion::getGid, dd.getString("gid"));
                qdesQuestionMapper.update(null, upQuWrapper);
            });
        }*/

        if (!forTemplet) {
            StatusDto statusDto = new StatusDto().setProjectId(proj.getId());
            qdesMessageSendService.projectUpdatedSend(statusDto);
        }
        // 判断是否为模板问卷
        if (proj.getPtype() == PROJECT_PTYPE_TEMPLET.getNumber()) {
            // 更新LibProject中题目数量question_count
            LambdaUpdateWrapper<QdesLibProject> projectLibWrapper = new LambdaUpdateWrapper<>();
            projectLibWrapper.set(QdesLibProject::getQuestionCount, allQids.size())
                    .set(QdesLibProject::getUpdatedDt, LocalDateTime.now())
                    .eq(QdesLibProject::getProjectId, proj.getId());
            qdesLibprojectMapper.update(null, projectLibWrapper);
        }
    }

    private void optionOrder(QdesQstruct sourceQdesQstruct, List<String> srcGids, JSONObject allQdict, QdesQstruct targetQdesQstruct) {
        // 引用选项顺序
        JSONObject dataJs = StringUtils.isEmpty(sourceQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(sourceQdesQstruct.getData());
        List<JSONObject> sourceOrders = dataJs.get("optionOrderRefs") == null ? new ArrayList<>()
                : JSONObject.parseArray(dataJs.getString("optionOrderRefs"), JSONObject.class);
        List<JSONObject> targetOrders = new ArrayList<>();

        for (JSONObject item : sourceOrders) {
            if (srcGids.contains(item.getString("sourceQGid")) && srcGids.contains(item.getString("targetQGid"))) {
                JSONObject newJs = new JSONObject();
                newJs.put("sourceQGid", getTargetGid(allQdict, item.getString("sourceQGid")));
                newJs.put("targetQGid", getTargetGid(allQdict, item.getString("targetQGid")));
                targetOrders.add(newJs);
            }
        }
        if (CollectionUtils.isNotEmpty(targetOrders)) {
            JSONObject tDataJs = StringUtils.isEmpty(targetQdesQstruct.getData()) ? new JSONObject()
                    : JSONObject.parseObject(targetQdesQstruct.getData());
            List<JSONObject> targetOrderRefs = tDataJs.get("optionOrderRefs") == null ? new ArrayList<>()
                    : JSONObject.parseArray(tDataJs.getString("optionOrderRefs"), JSONObject.class);
            targetOrderRefs.addAll(targetOrders);
            tDataJs.put("optionOrderRefs", targetOrderRefs);
            targetQdesQstruct.setData(JSONObject.toJSONString(tDataJs));
        }
    }

    private JSONObject answerRef(QdesQstruct sourceQdesQstruct, List<String> srcGids, JSONObject allQdict, QdesQstruct targetQdesQstruct) {
        // 答案引用复制
        JSONObject dataJs = StringUtils.isEmpty(sourceQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(sourceQdesQstruct.getData());
        JSONObject sourceAnswerRefs = dataJs.getJSONObject("answerRefs") == null ? new JSONObject()
                : dataJs.getJSONObject("answerRefs");
        JSONObject tDataJs = StringUtils.isEmpty(targetQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(targetQdesQstruct.getData());
        JSONObject targetAnswerRefs = tDataJs.getJSONObject("answerRefs") == null ? new JSONObject()
                : tDataJs.getJSONObject("answerRefs");
        JSONObject targetAnswerRefsDict = new JSONObject();
        JSONObject gidReftitleDic = new JSONObject();

        if (!sourceAnswerRefs.isEmpty()) {
            // 获取目标问卷所有的answerName，如果源问卷跟现在的answerName冲突，不导入
            List<String> targetAnswerNameList = new ArrayList<>();
            for (String gid : targetAnswerRefs.keySet()) {
                List<JSONObject> answerList = JSONObject.parseArray(targetAnswerRefs.getString(gid), JSONObject.class);
                if (CollectionUtils.isNotEmpty(answerList)) {
                    for (JSONObject item : answerList) {
                        targetAnswerNameList.add(item.getString("answerName"));
                    }
                }
            }
            for (String gid : sourceAnswerRefs.keySet()) {
                List<JSONObject> answerList = JSONObject.parseArray(sourceAnswerRefs.getString(gid), JSONObject.class);
                if (allQdict.containsKey(gid)) {
                    String targetGid = getTargetGid(allQdict, gid);
                    List<String> answerNameList = new ArrayList<>();
                    for (JSONObject item : answerList) {
                        if (srcGids.contains(item.getString("sourceQGid"))
                                && !targetAnswerNameList.contains(item.getString("answerName"))) {
                            item.put("sourceGid", getTargetGid(allQdict, item.getString("sourceGid")));
                            item.put("sourceQGid", getTargetGid(allQdict, item.getString("sourceQGid")));
                            item.put("targetGid", getTargetGid(allQdict, item.getString("targetGid")));
                            item.put("targetQGid", getTargetGid(allQdict, item.getString("targetQGid")));
                            if (targetAnswerRefsDict.containsKey(targetGid)) {
                                JSONArray tList = targetAnswerRefsDict.getJSONArray(targetGid);
                                tList = tList == null ? new JSONArray() : tList;
                                tList.add(item);
                                targetAnswerRefsDict.put(targetGid, tList);
                            } else {
                                JSONArray tList = new JSONArray();
                                tList.add(item);
                                targetAnswerRefsDict.put(targetGid, tList);
                            }
                        } else {
                            answerNameList.add(String.format("[%s]", item.getString("answerName")));
                        }
                    }
                    gidReftitleDic.put(targetGid, answerNameList);
                } else {
                    continue;
                }
                sourceAnswerRefs.put(gid, answerList);
            }
        }
        dataJs.put("answerRefs", sourceAnswerRefs);
        sourceQdesQstruct.setData(JSONObject.toJSONString(dataJs));

        if (!targetAnswerRefsDict.isEmpty()) {
            tDataJs.put("answerRefs", updateValue(tDataJs.getJSONObject("answerRefs"), targetAnswerRefsDict));
            targetQdesQstruct.setData(JSONObject.toJSONString(tDataJs));
        }
        return gidReftitleDic;
    }

    private JSONObject updateValue(JSONObject source, JSONObject target) {
        if (target == null || target.isEmpty()) {
            return source;
        }
        if (source == null || source.isEmpty()) {
            return target;
        }
        List<String> keys = new ArrayList<>(target.keySet());
        for (int i = 0; i < keys.size(); i++) {
            Object val = target.get(keys.get(i));
            source.put(keys.get(i), val);
        }
        return source;
    }

    private void questionDisplay(QdesQstruct sourceQdesQstruct, List<String> srcGids, JSONObject allQdict, QdesQstruct targetQdesQstruct) {
        // 显示逻辑复制
        JSONObject dataJs = StringUtils.isEmpty(sourceQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(sourceQdesQstruct.getData());
        List<JSONObject> sourceDisplayList = StringUtils.isEmpty(dataJs.getString("dispConsList")) ? new ArrayList<>()
                : JSONObject.parseArray(dataJs.getString("dispConsList"), JSONObject.class);
        // 目标结构表中的跳转
        JSONObject tDataJs = StringUtils.isEmpty(targetQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(targetQdesQstruct.getData());
        List<JSONObject> targetQstructDispList = StringUtils.isEmpty(tDataJs.getString("dispConsList")) ? new ArrayList<>()
                : JSONObject.parseArray(tDataJs.getString("dispConsList"), JSONObject.class);
        List<JSONObject> targetDisplayList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(sourceDisplayList)) {
            int targetDispMaxId = 0;
            for (JSONObject item : targetQstructDispList) {
                if (item.getInteger("id") != null && item.getInteger("id") > targetDispMaxId) {
                    targetDispMaxId = item.getInteger("id");
                }
            }
            for (JSONObject item : sourceDisplayList) {
                if (!srcGids.contains(item.getString("targetQ"))) {
                    continue;
                }
                JSONObject conditionGroup = item.getJSONObject("conditionGroup");
                if (conditionGroup != null) {
                    JSONArray condList = conditionGroup.getJSONArray("condList");
                    JSONArray newCondList = new JSONArray();
                    condList = condList == null ? new JSONArray() : condList;
                    for (int index = 0; index < condList.size(); index++) {
                        JSONObject cond = condList.getJSONObject(index);
                        if (srcGids.contains(cond.getString("sourceQ"))) {
                            cond.put("sourceQ", getTargetGid(allQdict, cond.getString("sourceQ")));
                            if (cond.get("value") != null) {
                                if (cond.get("value") instanceof String) {
                                    String val = cond.getString("value");
                                    if (val.contains(",")) {
                                        List<String> condValList = new ArrayList<>();
                                        String[] valList = val.split(",");
                                        for (String valItem : valList) {
                                            condValList.add(getTargetGid(allQdict, valItem));
                                        }
                                        cond.put("value", String.join(",", condValList));
                                    } else if (!val.contains("|")) {
                                        cond.put("value", getTargetGid(allQdict, cond.getString("value")));
                                    }
                                } else {
                                    cond.put("value", getTargetGid(allQdict, cond.getString("value")));
                                }
                            }
                            newCondList.add(cond);
                        }
                    }
                    if (CollectionUtils.isEmpty(newCondList)) {
                        continue;
                    }
                    item.put("targetQ", getTargetGid(allQdict, item.getString("targetQ")));
                    targetDispMaxId++;
                    item.put("id", targetDispMaxId);
                    targetDisplayList.add(item);
                }
            }
            dataJs.put("dispConsList", sourceDisplayList);
            sourceQdesQstruct.setData(dataJs.toJSONString());
        }
        if (CollectionUtils.isNotEmpty(targetDisplayList)) {
            targetQstructDispList.addAll(targetDisplayList);
            tDataJs.put("dispConsList", targetQstructDispList);
            targetQdesQstruct.setData(tDataJs.toJSONString());
        }
    }

    private void questionJump(QdesQstruct sourceQdesQstruct, List<String> srcGids, JSONObject allQdict, QdesQstruct targetQdesQstruct) {
        // 题目跳转逻辑拷贝
        JSONObject dataJs = StringUtils.isEmpty(sourceQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(sourceQdesQstruct.getData());
        List<JSONObject> sourceJumpList = StringUtils.isEmpty(dataJs.getString("jumpConsList")) ? new ArrayList<>()
                : JSONObject.parseArray(dataJs.getString("jumpConsList"), JSONObject.class);
        List<JSONObject> targetJumpList = new ArrayList<>();
        // 目标结构表中的跳转
        JSONObject tDataJs = StringUtils.isEmpty(targetQdesQstruct.getData()) ? new JSONObject()
                : JSONObject.parseObject(targetQdesQstruct.getData());
        List<JSONObject> targetQstructJumpList = StringUtils.isEmpty(tDataJs.getString("jumpConsList")) ? new ArrayList<>()
                : JSONObject.parseArray(tDataJs.getString("jumpConsList"), JSONObject.class);
        if (CollectionUtils.isNotEmpty(sourceJumpList)) {
            // 获取目标结构跳转逻辑中项目最大id
            int targetJumpMaxId = 0;
            for (JSONObject item : targetQstructJumpList) {
                if (item.getInteger("id") != null && item.getInteger("id") > targetJumpMaxId) {
                    targetJumpMaxId = item.getInteger("id");
                }
            }
            for (JSONObject item : sourceJumpList) {
                List<String> statusList = Arrays.asList("finished", "screen_out");
                if (srcGids.contains(item.getString("bindQ")) && (srcGids.contains(item.getString("nextQ"))
                        || statusList.contains(item.getString("nextQ")))) {
                    // 找到对应的目标题目中的gid
                    String targetBindQ = getTargetGid(allQdict, item.getString("bindQ"));
                    if (!statusList.contains(item.getString("bindQ"))) {
                        String targetNextQ = getTargetGid(allQdict, item.getString("nextQ"));
                        item.put("nextQ", targetNextQ);
                    }
                    item.put("bindQ", targetBindQ);
                    // 修改condList
                    if (StringUtils.isNotEmpty(item.getString("conditionGroup"))) {
                        List<JSONObject> conditonGroupList = JSONObject.parseArray(item.getString("conditionGroup"), JSONObject.class);
                        for (JSONObject conditionGroup : conditonGroupList) {
                            List<JSONObject> conditionList = StringUtils.isEmpty(conditionGroup
                                    .getString("condList")) ? new ArrayList<>() : JSONObject
                                    .parseArray(conditionGroup.getString("condList"), JSONObject.class);
                            for (JSONObject cond : conditionList) {
                                cond.put("sourceQ", targetBindQ);
                                if (cond.get("value") != null) {
                                    if (cond.get("value") instanceof String) {
                                        String val = cond.getString("value");
                                        if (val.contains(",")) {
                                            List<String> condValList = new ArrayList<>();
                                            String[] valList = val.split(",");
                                            for (String valItem : valList) {
                                                condValList.add(getTargetGid(allQdict, valItem));
                                            }
                                            cond.put("value", String.join(",", condValList));
                                        } else if (!val.contains("|")) {
                                            cond.put("value", getTargetGid(allQdict, cond.getString("value")));
                                        }
                                    } else {
                                        cond.put("value", getTargetGid(allQdict, cond.getString("value")));
                                    }
                                }
                            }
                            conditionGroup.put("condList", conditionList);
                        }
                        item.put("conditionGroup", conditonGroupList);
                    }
                    targetJumpMaxId++;
                    item.put("id", targetJumpMaxId);
                    targetJumpList.add(item);
                }
            }
            dataJs.put("jumpConsList", sourceJumpList);
            sourceQdesQstruct.setData(dataJs.toJSONString());
        }
        if (CollectionUtils.isNotEmpty(targetJumpList)) {
            targetQstructJumpList.addAll(targetJumpList);
            tDataJs.put("jumpConsList", targetQstructJumpList);
            targetQdesQstruct.setData(JSONObject.toJSONString(tDataJs));
        }
    }

    private void allInsertItems(QdesQstruct targetQdesQstruct, int page, Integer judgeIndex, List<JSONObject> insertItemQuestions) {
        // 分组、分页模式都需要插入结构表对应页面的题目中
        List<JSONObject> questions = new ArrayList<>();
        JSONObject dataJs = StringUtils.isEmpty(targetQdesQstruct.getData()) ? new JSONObject() : JSONObject.parseObject(targetQdesQstruct.getData());
        JSONArray items = dataJs.getJSONArray("items");
        items = items == null ? new JSONArray() : items;
        JSONObject pageJs = items.getJSONObject(page);
        pageJs = pageJs == null ? new JSONObject() : pageJs;
        questions = pageJs.getString("items") == null ? questions : JSONObject.parseArray(pageJs.getString("items"), JSONObject.class);
        // 拆分数组
        if (CollectionUtils.isNotEmpty(questions)) {
            int tmpJudgeIndex = judgeIndex == null ? questions.size() : judgeIndex;
            List<JSONObject> frontQuestionItems = Optional.of(questions.subList(0, tmpJudgeIndex)).orElse(new ArrayList<>());
            List<JSONObject> lastQuestionItems = questions.size() >= tmpJudgeIndex + 1 ? questions.subList(tmpJudgeIndex, questions.size())
                    : new ArrayList<>();
            tmpJudgeIndex += insertItemQuestions.size();

            List<JSONObject> lastItems = new ArrayList<>();
            for (JSONObject item : lastQuestionItems) {
                tmpJudgeIndex++;
                item.put("_index", tmpJudgeIndex);
                lastItems.add(item);
            }
            if (CollectionUtils.isNotEmpty(insertItemQuestions)) {
                frontQuestionItems.addAll(insertItemQuestions);
            }
            if (CollectionUtils.isNotEmpty(lastItems)) {
                frontQuestionItems.addAll(lastItems);
            }
            pageJs.put("items", frontQuestionItems);
            items.set(page, pageJs);
            dataJs.put("items", items);
            targetQdesQstruct.setData(dataJs.toJSONString());
        } else {
            pageJs.put("items", insertItemQuestions);
            items.set(page, pageJs);
            dataJs.put("items", items);
            targetQdesQstruct.setData(dataJs.toJSONString());
        }
    }

    private void groupInsertSorts(JSONObject resdic, QdesQstruct targetQdesQstruct, Object judgeGid, List<JSONObject> insertGroupQuestions) {
        if (resdic.getBoolean("isgroup")) {
            String key = resdic.getString("groupid");
            JSONObject dataJs = JSONObject.parseObject(targetQdesQstruct.getData()) == null
                    ? new JSONObject() : JSONObject.parseObject(targetQdesQstruct.getData());
            JSONObject groupJs = dataJs.getJSONObject("groups") == null ? new JSONObject() : dataJs.getJSONObject("groups");
            JSONObject groups = groupJs.getJSONObject(key);
            if (groups == null || groups.isEmpty()) {
                JSONObject targetJs = new JSONObject();
                targetJs.put("sorts", insertGroupQuestions);
                groupJs.put("1", targetJs);
                dataJs.put("groups", groupJs);
                targetQdesQstruct.setData(JSONObject.toJSONString(dataJs));
            } else {
                String sorts = groups.getString("sorts");
                if (StringUtils.isNotEmpty(sorts)) {
                    List<JSONObject> sortJs = JSONObject.parseArray(sorts, JSONObject.class);
                    sortJs.addAll(insertGroupQuestions);
                    groups.put("sorts", sortJs);
                } else {
                    groups.put("data", "[]");
                    groups.put("sorts", insertGroupQuestions);
                }
                dataJs.put("groups", groupJs);
                targetQdesQstruct.setData(dataJs.toJSONString());
            }

        }
    }

    private int getDispatchCounts(List<QdesQuestion> sourceQuestions) {
        int dispatchCounts = 0;
        for (QdesQuestion question : sourceQuestions) {
            dispatchCounts++;
            JSONObject qDict = StringUtils.isEmpty(question.getData()) ? new JSONObject() : JSONObject
                    .parseObject(question.getData());
            JSONArray items = qDict.getJSONArray("items") == null ? new JSONArray() : qDict.getJSONArray("items");
            JSONArray rowsItems = qDict.getJSONArray("rows_items") == null ? new JSONArray() : qDict.getJSONArray("rows_items");
            JSONArray optionList = qDict.getJSONArray("option_list") == null ? new JSONArray() : qDict.getJSONArray("option_list");
            dispatchCounts += items.size() + rowsItems.size() + optionList.size();
        }
        return dispatchCounts;
    }

    private JSONObject getInfo(QdesQstruct targetStruct, QdesProject targetProject, Integer dispatchCounts, Integer judgeGid) {
        // 获取要分配的gids
        // 分组模式：获取目标gid的index、分组的groupid、所在页(默认0)
        // 分页模式：获取目标gid的index、所在页
        JSONObject resdic = new JSONObject();
        String openGrouping = StringUtils.isEmpty(targetProject.getCustomAttr()) ? null : JSONObject
                .parseObject(targetProject.getCustomAttr()).getString("openGrouping"); // 0:分页 1：分组
        if ("1".equals(openGrouping)) {
            resdic.put("isgroup", true);
        } else {
            resdic.put("isgroup", false);
        }
        resdic.put("groupid", 1);
        resdic.put("index", 1);
        resdic.put("page", 0);
        boolean isNewpage = false; // 标记是否是新增页面，如果是，分配gid数量需要多分一个
        int dispatchGidIndex = 20; // 默认从第21个开始,跳过acquire_project_gid前面分配的gid,如果新页面，页面会占据一个gid

        if (judgeGid == null) {
            List<JSONObject> tmpItems = StringUtils.isEmpty(targetStruct.getData()) ? new ArrayList<>() : JSONObject
                    .parseArray(JSONObject.parseObject(targetStruct.getData()).getString("items"), JSONObject.class);
            tmpItems = tmpItems == null ? new ArrayList<>() : tmpItems;
            Collections.reverse(tmpItems);
            boolean hasQuestion = false;
            JSONObject pageDict = tmpItems.get(0);
            List<JSONObject> questions = StringUtils.isEmpty(pageDict.getString("items")) ? new ArrayList<>()
                    : JSONObject.parseArray(pageDict.getString("items"), JSONObject.class);
            if (!CollectionUtils.isEmpty(questions)) {
                hasQuestion = true;
                JSONObject question = questions.get(questions.size() - 1);
                if ("1".equals(openGrouping)) {
                    resdic.put("groupid", question.get("pGroupGid"));
                }
                resdic.put("index", question.get("_index"));
                resdic.put("page", tmpItems.size() - 1);
            }
            // 没有题目，两种情况：1、新建的项目，struct.data["items"]是空 2、struct.data["items"]最后一页是空页面，子items是空
            if (!hasQuestion) {
                if (CollectionUtils.isEmpty(tmpItems)) {
                    isNewpage = true;
                    JSONArray itemArray = new JSONArray();
                    itemArray.add(getPageJson());
                    JSONObject dataJson = StringUtils.isEmpty(targetStruct.getData()) ? new JSONObject()
                            : JSONObject.parseObject(targetStruct.getData());
                    dataJson.put("items", itemArray);
                    targetStruct.setData(dataJson.toJSONString());
                } else {
                    resdic.put("page", tmpItems.size() - 1);
                }
            }
        }
        dispatchCounts += 20;
        if (isNewpage) {
            dispatchCounts++;
            dispatchGidIndex = 21;
        }
        resdic.put("dispatch_gids", acquireProjectGid(targetStruct.getProjectId(), dispatchCounts));
        if (isNewpage) {
            if (StringUtils.isNotEmpty(resdic.getString("dispatch_gids"))) {
                JSONObject dataJson = JSONObject.parseObject(targetStruct.getData());
                List<JSONObject> itemArray = JSONObject.parseArray(dataJson.getString("items"), JSONObject.class);
                itemArray.stream().findFirst().ifPresent(firstJson -> {
                    firstJson.put("gid", JSONObject.parseArray(resdic.getString("dispatch_gids")).get(20));
                    dataJson.put("items", itemArray);
                    targetStruct.setData(dataJson.toJSONString());
                });
                /*if (!CollectionUtils.isEmpty(itemArray)) {
                    JSONObject firstJson = itemArray.get(0);

                    firstJson.put("gid", JSONObject.parseArray(resdic.getString("dispatch_gids")).get(20));
                    dataJson.put("items", itemArray);
                    targetStruct.setData(dataJson.toJSONString());
                }*/
            }
        }
        resdic.put("dispatch_gid_index", dispatchGidIndex);
        return resdic;
    }

    public List<Integer> acquireProjectGid(String projectId, Integer num) {
        // 获取问卷gid，支持批量获取
        Integer maxGid = serviceSequenceService.getSequence("proj_gid_" + projectId, num);
        if (maxGid == null) {
            QdesProject project = qdesProjectMapper.selectById(projectId);
            maxGid = 0;
            if (project != null) {
                QdesQstruct qdesQstruct = qdesQstructMapper.selectById(project.getStructId());
                if (qdesQstruct != null) {
                    JSONObject datajs = StringUtils.isEmpty(qdesQstruct.getData()) ? new JSONObject()
                            : JSONObject.parseObject(qdesQstruct.getData());
                    maxGid = datajs.getInteger("max_gid") == null ? 0 : datajs.getInteger("max_gid");
                }
            }
            maxGid += num;
            maxGid = serviceSequenceService.getSequence("proj_gid_" + projectId, maxGid);
        }
        List<Integer> allGids = new ArrayList<>();
        for (int i = num - 1; i >= 0; i--) {
            allGids.add(maxGid - i);
        }
        return allGids;
    }

    private int getMaxIndex(QdesQstruct targetStruct) {
        // 获取目标所有题目中最大index
        int maxIndex = 0;
        String data = targetStruct.getData();
        if (StringUtils.isNotEmpty(data)) {
            JSONObject dataJs = JSONObject.parseObject(data);
            if (StringUtils.isNotEmpty(dataJs.getString("items"))) {
                List<JSONObject> items = JSONObject.parseArray(dataJs.getString("items"), JSONObject.class);
                for (JSONObject item : items) {
                    if (item.getInteger("_index") != null && item.getInteger("_index") > maxIndex) {
                        maxIndex = item.getInteger("_index");
                    }
                }
            }
        }
        return maxIndex;
    }

    private JSONObject getPageJson() {
        String attr = ResourceUtil.readUtf8Str(PAGE_FILE_PATH);
        return JSON.parseObject(attr);
    }

    private void dispatchGid(List<QdesQuestion> sourceQuestions, List<QdesQuestion> targetQuestions, String gidsStr,
                             JSONObject quotaDdict, JSONObject allQdict, List<String> newCids, JSONObject qcidQgidDict,
                             int targetMaxIndex, int gidIndex) {
        // 修改源题目和选项gid为新的gid
        int tmpJudgeIndex = targetMaxIndex;
        List<Integer> gids = StringUtils.isEmpty(gidsStr) ? new ArrayList<>() : JSONObject.parseArray(gidsStr, Integer.class);
        for (QdesQuestion question : sourceQuestions) {
            boolean isNeedQuota = false;  // 是否需要配额标记
            if (StringUtils.isNotEmpty(question.getData())
                    && ("single".equals(JSONObject.parseObject(question.getData()).getString("custom_qtype"))
                    || "multiple".equals(JSONObject.parseObject(question.getData()).getString("custom_qtype")))) {
                isNeedQuota = true;
            }
            JSONObject qDdict = StringUtils.isEmpty(question.getData()) ? new JSONObject()
                    : JSONObject.parseObject(question.getData());
            tmpJudgeIndex++;
            Integer qgid = gids.get(gidIndex);
            String qcid = getCid(targetQuestions, newCids);
            newCids.add(qcid);
            qcidQgidDict.put(qcid, qDdict);
            gidIndex++;
            // 源目gid对应关系
            List<String> qgidList = quotaDdict.getString("qgid_list") == null ? new ArrayList<>()
                    : JSONObject.parseArray(quotaDdict.getString("qgid_list"), String.class);
            qgidList.add(qDdict.getString("gid"));
            quotaDdict.put("qgid_list", qgidList);

            JSONObject qgidMap = quotaDdict.getJSONObject("qtitle_map") == null ? new JSONObject()
                    : quotaDdict.getJSONObject("qtitle_map");
            JSONObject newMap = new JSONObject();
            newMap.put("title", qDdict.getString("title"));
            newMap.put("cid", qcid);
            qgidMap.put(qDdict.getString("gid"), newMap);
            quotaDdict.put("qtitle_map", qgidMap);

            JSONObject qcidMap = quotaDdict.getJSONObject("qcid_map") == null ? new JSONObject()
                    : quotaDdict.getJSONObject("qcid_map");
            if (isNeedQuota) {
                qcidMap.put(qDdict.getString("cid"), qcid);
                qcidMap.put(qDdict.getString("gid"), qgid);
            }
            quotaDdict.put("qcid_map", qcidMap);
            allQdict.put(qDdict.getString("gid"), qgid);
            qDdict.put("gid", qgid);
            qDdict.put("_index", tmpJudgeIndex);
            qDdict.put("seq", qgid);
            qDdict.put("cid", qcid);

            List<JSONObject> items = qDdict.getString("items") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("items"), JSONObject.class);
            List<JSONObject> optionGroupList = qDdict.getString("option_group_list") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("option_group_list"), JSONObject.class);
            List<JSONObject> optionBindList = qDdict.getString("option_bind_list") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("option_bind_list"), JSONObject.class);
            List<JSONObject> optionExcludeList = qDdict.getString("option_exclude_list") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("option_exclude_list"), JSONObject.class);
            List<JSONObject> rowsItems = qDdict.getString("rows_items") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("rows_items"), JSONObject.class);
            List<JSONObject> rowGroupList = qDdict.getString("row_group_list") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("row_group_list"), JSONObject.class);
            List<JSONObject> rowBindList = qDdict.getString("row_bind_list") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("row_bind_list"), JSONObject.class);
            List<JSONObject> optionList = qDdict.getString("option_list") == null ? new ArrayList<>() : JSONObject
                    .parseArray(qDdict.getString("option_list"), JSONObject.class);

            JSONObject ogidMap = quotaDdict.getJSONObject("ogid_map") == null ? new JSONObject()
                    : quotaDdict.getJSONObject("ogid_map");
            quotaDdict.put("ogid_map", ogidMap);

            for (JSONObject itemDict : items) {
                Integer gid = gids.get(gidIndex);
                gidIndex++;
                String oid = itemDict.getString("oid");
                if (isNeedQuota) {
                    ogidMap.put(itemDict.getString("gid"), gid);
                }
                allQdict.put(itemDict.getString("gid"), gid);
                itemDict.put("gid", gid);
                itemDict.put("seq", gid);
                itemDict.put("oid", oid);
            }

            JSONObject ogidDict = new JSONObject();
            for (JSONObject item : items) {
                ogidDict.put(item.getString("oid"), item.get("gid"));
            }
            JSONObject ogroupDict = new JSONObject();
            // 选项组 补id
            for (int oIndex = 0; oIndex < optionGroupList.size(); oIndex++) {
                int ogIndex = oIndex + 1;
                JSONObject itemDict = optionGroupList.get(oIndex);
                String ogroupId = qgid + "_" + ogIndex;
                List<JSONObject> itemList = JSONObject.parseArray(itemDict.getString("items"), JSONObject.class);
                itemList = itemList == null ? new ArrayList<>() : itemList;
                for (JSONObject oitemDict : itemList) {
                    Object tmpOgid = ogidDict.get(oitemDict.getString("oid"));
                    oitemDict.put("gid", tmpOgid);
                    ogroupDict.put(oitemDict.getString("oid"), ogroupId);
                }
                // 其他分组 id 命名:qgid_OTHER_GROUP
                if (optionGroupList.size() == ogIndex && "其他分组".equals(itemDict.getString("title"))) {
                    ogroupId = qgid + "_OTHER_GROUP";
                }
                itemDict.put("items", itemList);
                itemDict.put("id", ogroupId);
            }
            if (!optionGroupList.isEmpty()) {
                qDdict.put("option_group_list", optionGroupList);
            }

            // 选项绑定 补id
            for (int oIndex = 0; oIndex < optionBindList.size(); oIndex++) {
                int obIndex = oIndex + 1;
                JSONObject itemDict = optionBindList.get(oIndex);
                String obindId = qgid + "_" + obIndex;
                List<JSONObject> itemList = JSONObject.parseArray(itemDict.getString("items"), JSONObject.class);
                itemList = itemList == null ? new ArrayList<>() : itemList;
                for (JSONObject oitemDict : itemList) {
                    Object tmpOgid = ogidDict.get(oitemDict.getString("oid"));
                    oitemDict.put("gid", tmpOgid);
                    String pGroupId = ogroupDict.getString(oitemDict.getString("oid"));
                    if (StringUtils.isNotEmpty(pGroupId)) {
                        oitemDict.put("pGroupId", pGroupId);
                    }
                }
                itemDict.put("items", itemList);
                itemDict.put("id", obindId);
            }
            if (!optionBindList.isEmpty()) {
                qDdict.put("option_bind_list", optionBindList);
            }

            // 选项排他 补id
            for (int oeIndex = 0; oeIndex < optionExcludeList.size(); oeIndex++) {
                JSONObject optionExcludeDict = optionExcludeList.get(oeIndex);
                String oexcludeId = qgid + "_" + (oeIndex + 1);
                List<JSONObject> oeItems = JSONObject.parseArray(optionExcludeDict.getString("items"), JSONObject.class);
                oeItems = oeItems == null ? new ArrayList<>() : oeItems;
                for (JSONObject temItemDict : oeItems) {
                    Object tmpOid = ogidDict.get(temItemDict.getString("oid"));
                    temItemDict.put("gid", tmpOid);
                }
                optionExcludeDict.put("items", oeItems);
                optionExcludeDict.put("id", oexcludeId);
            }
            if (!optionExcludeList.isEmpty()) {
                qDdict.put("option_exclude_list", optionExcludeList);
            }

            // 矩阵行 补id 复制oid
            for (JSONObject itemDict : rowsItems) {
                Integer ogid = gids.get(gidIndex);
                gidIndex++;
                String oid = itemDict.getString("oid");
                // 源目gid对应关系
                if (isNeedQuota) {
                    ogidMap.put(itemDict.getString("gid"), ogid);
                }
                allQdict.put(itemDict.getString("gid"), ogid);
                itemDict.put("gid", ogid);
                itemDict.put("seq", ogid);
                itemDict.put("oid", oid);
            }
            // 矩阵行oid: ogid 字典
            JSONObject rgidDict = new JSONObject();
            rowsItems.forEach(rowsItem -> {
                rgidDict.put(rowsItem.getString("oid"), rowsItem.get("gid"));
            });
            if (!rowsItems.isEmpty()) {
                qDdict.put("rows_items", rowsItems);
            }

            JSONObject rgroupDict = new JSONObject();
            // 矩阵行分组 补id
            for (int i = 0; i < rowGroupList.size(); i++) {
                int rgIndex = i + 1;
                JSONObject rgItemDict = rowGroupList.get(i);
                String rgroupId = qgid + "_" + rgIndex;
                List<JSONObject> ritems = JSONObject.parseArray(rgItemDict.getString("items"), JSONObject.class);
                ritems = ritems == null ? new ArrayList<>() : ritems;
                for (JSONObject ritemDict : ritems) {
                    Object tmpRgid = rgidDict.get(ritemDict.getString("oid"));
                    ritemDict.put("gid", tmpRgid);
                    rgroupDict.put(ritemDict.getString("oid"), rgroupId);
                }
                if (rowGroupList.size() == rgIndex && "其他分组".equals(rgItemDict.getString("title"))) {
                    rgroupId = qgid + "_OTHER_GROUP";
                }
                rgItemDict.put("items", ritems);
                rgItemDict.put("id", rgroupId);
            }
            if (!rowGroupList.isEmpty()) {
                qDdict.put("row_group_list", rowGroupList);
            }

            // 矩阵行绑定 补id
            for (int rbIndex = 0; rbIndex < rowBindList.size(); rbIndex++) {
                JSONObject rowBindDict = rowBindList.get(rbIndex);
                String rbindId = qgid + "_" + (rbIndex + 1);
                List<JSONObject> rbItems = JSONObject.parseArray(rowBindDict.getString("items"), JSONObject.class);
                rbItems = rbItems == null ? new ArrayList<>() : rbItems;
                for (JSONObject tmpItemDict : rbItems) {
                    String tmpRoid = tmpItemDict.getString("oid");
                    Object tmpRgid = rgidDict.get(tmpRoid);
                    String pGroupId = rgroupDict.getString(tmpRoid);
                    tmpItemDict.put("gid", tmpRgid);
                    if (StringUtils.isNotEmpty(pGroupId)) {
                        tmpItemDict.put("pGroupId", pGroupId);
                    }
                }
                rowBindDict.put("items", rbItems);
                rowBindDict.put("id", rbindId);
            }
            if (!rowBindList.isEmpty()) {
                qDdict.put("row_bind_list", rowBindList);
            }

            // 级联题 补gid
            for (JSONObject optionDict : optionList) {
                Integer ogid = gids.get(gidIndex);
                gidIndex++;
                String oid = optionDict.getString("oid");
                allQdict.put(optionDict.getString("gid"), ogid);
                optionDict.put("gid", ogid);
                optionDict.put("seq", ogid);
                optionDict.put("oid", oid);
            }
            if (!optionList.isEmpty()) {
                qDdict.put("option_list", optionList);
            }

            question.setData(qDdict.toJSONString());
        }
    }

    private String getCid(List<QdesQuestion> targetQuestions, List<String> newCids) {
        // 源cid是否和目标问题中的cid冲突
        int cidNum = 0;
        for (QdesQuestion question : targetQuestions) {
            String data = question.getData();
            if (StringUtils.isNotEmpty(data)) {
                String cid = JSONObject.parseObject(data).getString("cid");
                String tmpStr = ReUtil.getGroup1("Q(\\d+)", cid);
                int tmpNum = StringUtils.isEmpty(tmpStr) ? 0 : Integer.parseInt(tmpStr);
                if (tmpNum > cidNum) {
                    cidNum = tmpNum;
                }
            }
        }
        for (String cid : newCids) {
            String tmpStr = ReUtil.getGroup1("Q(\\d+)", cid);
            int tmpNum = StringUtils.isEmpty(tmpStr) ? 0 : Integer.parseInt(tmpStr);
            if (tmpNum > cidNum) {
                cidNum = tmpNum;
            }
        }
        // 不管是否和现有的冲突，从最大q下标开始
        cidNum++;
        String cid = "Q" + cidNum;
        return cid;
    }

    private JSONObject cellDisabled(JSONObject sourceQuestionData, JSONObject allQdict) {
        // 矩阵题检测单元格不可填写设置
        JSONObject sourceAllowDict = sourceQuestionData.getJSONObject("allowNull");
        JSONObject targetAllowDict = new JSONObject();
        if (sourceAllowDict != null) {
            sourceAllowDict.keySet().forEach(key -> {
                String val = sourceAllowDict.getString(key);
                List<String> tmpSourceGids = Arrays.asList(key.split("__"));
                List<String> tmpTargetGids = new ArrayList<>();
                tmpSourceGids.forEach(gid -> {
                    String targetGid = getTargetGid(allQdict, gid);
                    tmpTargetGids.add(targetGid);
                });
                String targetKey = String.join("__", tmpTargetGids);
                targetAllowDict.put(targetKey, val);
            });
            if (!targetAllowDict.isEmpty()) {
                sourceQuestionData.put("allowNull", targetAllowDict);
            }
        }
        return sourceQuestionData;
    }

    private String getTargetGid(JSONObject allQdict, String srcGid) {
        // 根据源题目gid找到目标题目gid
        String targetGid = allQdict.getString(srcGid);
        if (StringUtils.isEmpty(targetGid)) {
            throw new BusinessException("find gid error");
        }
        return targetGid;
    }

    private QdesQstruct getQstructByVersion(String projectId, Integer version) {
        // 获取struct
        LambdaQueryWrapper<QdesQstruct> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesQstruct::getProjectId, projectId)
                .eq(QdesQstruct::getVersion, version)
                .last("limit 1");
        QdesQstruct struct = qdesQstructMapper.selectOne(queryWrapper);
        if (Objects.isNull(struct)) {
            log.error("getQstructByVersion qstruct[projectID={},version={}] not found", projectId, version);
            return null;
        }
        // 获取questions
        LambdaQueryWrapper<QdesQuestion> questionWrapper = Wrappers.lambdaQuery();
        questionWrapper.eq(QdesQuestion::getStructId, struct.getId());
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        this.fillQstructData(struct, qdesQuestions);
        return struct;
    }

    private void fillQstructData(QdesQstruct qdesQstruct, List<QdesQuestion> qdesQuestions) {
        String sData = qdesQstruct.getData();
        if (StringUtils.isNotEmpty(sData)) {
            Map<Integer, String> questions = new HashMap<>();
            qdesQuestions.forEach(q -> {
                JSONObject dataJson = new JSONObject();
                String data = q.getData();
                if (StringUtils.isEmpty(data)) {
                    dataJson.put("id", q.getId());
                } else {
                    dataJson = JSONObject.parseObject(data);
                    dataJson.put("id", q.getId());
                }
                questions.put(q.getGid(), JSONObject.toJSONString(dataJson));
            });
            qdesQstruct.setData(this.parseItem(questions, sData));
        }
    }

    private String parseItem(Map<Integer, String> questions, String itemStr) {
        if (NumberUtils.isDigits(itemStr)) {
            return questions.get(Integer.valueOf(itemStr));
        }
        JSONObject item = JSONObject.parseObject(itemStr);
        String type = item.getString("type");
        if (StringUtils.isEmpty(type)) {
            Integer gid = item.getInteger("gid");
            if (gid != null) {
                JSONObject item2 = JSONObject.parseObject(questions.get(gid));
                if (item2 != null) {
                    //item.keySet().forEach(key -> {
                    //    item2.put(key, item.get(key));
                    //});
                    item2.putAll(item);
                }
                return JSONObject.toJSONString(item2);
            }
        } else if ("G".equals(type)) {
            if (item.get("items") == null) {
                throw new BusinessException("group[gid=" + item.get("gid") + "] must have :items field");
            }
            List<String> array = JSONObject.parseArray(item.getString("items"), String.class);
            List<JSONObject> collect = array.stream().map(oneItem -> JSONObject.parseObject(parseItem(questions,
                    oneItem))).filter(oneItem -> !oneItem.isEmpty()).collect(Collectors.toList());
            item.put("items", collect);
            return JSONObject.toJSONString(item);
        } else if ("Q".equals(type)) {
            JSONObject valueJo = JSONObject.parseObject(questions.get(item.getInteger("gid")));
            item.put("id", valueJo == null ? null : valueJo.get("id"));
            return JSONObject.toJSONString(item);
        }
        return itemStr;
    }
}